diff --git a/.dockerignore b/.dockerignore index 38a45ad430b..bca449c8d7c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,7 @@ .github/ .git .dockerignore +.gitignore .bundle .env* .buildpacks @@ -9,24 +10,24 @@ .*ignore *.md *.log -Vagrantfile -Dockerfile +docker/prod/Dockerfile +docker/ci/Dockerfile Guardfile docker-compose.* browserslist -docs -!docs/api/apiv3/openapi-spec.yml -!docs/api/apiv3/paths -!docs/api/apiv3/tags -!docs/api/apiv3/components +/docs +!/docs/api/apiv3/openapi-spec.yml +!/docs/api/apiv3/paths +!/docs/api/apiv3/tags +!/docs/api/apiv3/components extra features help log/*.log spec -tmp +/tmp frontend/.angular/cache frontend/node_modules node_modules diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7b2d992740b..dd2051e2508 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -22,14 +22,18 @@ jobs: strategy: matrix: include: - - base_prefix: - platform: linux/amd64 - - base_prefix: ppc64le/ - platform: linux/ppc64le + - platform: linux/amd64 + target: slim + - platform: linux/arm64/v8 + target: slim + - platform: linux/amd64 + target: all-in-one + - platform: linux/ppc64le bim_support: false - - base_prefix: arm64v8/ - platform: linux/arm64/v8 + target: all-in-one + - platform: linux/arm64/v8 bim_support: false + target: all-in-one steps: - name: Checkout uses: actions/checkout@v3 @@ -57,8 +61,8 @@ jobs: with: context: . platforms: ${{ matrix.platform }} + target: ${{ matrix.target }} build-args: | - BASE_PREFIX=${{ matrix.base_prefix }} BIM_SUPPORT=${{ matrix.bim_support }} pull: true load: true @@ -67,7 +71,7 @@ jobs: - name: Test # We only test the native container. If that fails the builds for the others # will be cancelled as well. - if: matrix.platform == 'linux/amd64' + if: matrix.platform == 'linux/amd64' && matrix.target == 'all-in-one' run: | docker run \ --name openproject \ @@ -86,8 +90,8 @@ jobs: with: context: . platforms: ${{ matrix.platform }} + target: ${{ matrix.target }} build-args: | - BASE_PREFIX=${{ matrix.base_prefix }} BIM_SUPPORT=${{ matrix.bim_support }} labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true @@ -99,20 +103,29 @@ jobs: - name: Upload digest uses: actions/upload-artifact@v3 with: - name: digests + name: digests-${{ matrix.target }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 merge: runs-on: ubuntu-latest + strategy: + matrix: + target: [slim, all-in-one] needs: - build steps: - name: Download digests uses: actions/download-artifact@v3 with: - name: digests + name: digests-${{ matrix.target }} path: /tmp/digests + - name: Set suffix + id: set_suffix + run: | + suffix="-${{ matrix.target }}" + if [ "$suffix" = "-all-in-one" ]; then suffix="" ; fi + echo "suffix=$suffix" >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Docker meta @@ -122,11 +135,12 @@ jobs: images: ${{ env.REGISTRY_IMAGE }} flavor: | latest=false + suffix=${{ steps.set_suffix.outputs.suffix }} tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - type=raw,value=dev,enable={{is_default_branch}} + type=raw,value=dev,priority=200,enable={{is_default_branch}} - name: Login to Docker Hub uses: docker/login-action@v2 with: diff --git a/.github/workflows/pullpreview.yml b/.github/workflows/pullpreview.yml index 6456db167c4..c55088cc8cf 100644 --- a/.github/workflows/pullpreview.yml +++ b/.github/workflows/pullpreview.yml @@ -31,6 +31,7 @@ jobs: echo "OPENPROJECT_SHOW__SETTING__MISMATCH__WARNING=false" >> .env.pullpreview echo "OPENPROJECT_FEATURE__STORAGES__MODULE__ACTIVE=true" >> .env.pullpreview echo "OPENPROJECT_FEATURE__SHOW__CHANGES__ACTIVE=true" >> .env.pullpreview + echo "OPENPROJECT_LOOKBOOK__ENABLED=true" >> .env.pullpreview echo "OPENPROJECT_HSTS=false" >> .env.pullpreview - name: Boot as BIM edition if: contains(github.ref, 'bim/') || contains(github.head_ref, 'bim/') diff --git a/Gemfile b/Gemfile index 36a52408b2c..0aacdf80c20 100644 --- a/Gemfile +++ b/Gemfile @@ -114,9 +114,6 @@ gem 'mail', '= 2.8.1' # provide compatible filesystem information for available storage gem 'sys-filesystem', '~> 1.4.0', require: false -# Faster posix-compliant spawns for 8.0. conversions with pandoc -gem 'posix-spawn', '~> 0.3.13', require: false - gem 'bcrypt', '~> 3.1.6' gem 'multi_json', '~> 1.15.0' @@ -209,6 +206,8 @@ gem "store_attribute", "~> 1.0" gem "appsignal", "~> 3.0", require: false gem 'view_component' +# Lookbook +gem 'lookbook', '~> 2.0.5' gem 'turbo-rails', "~> 1.1" @@ -237,6 +236,9 @@ group :test do # XML comparison tests gem 'compare-xml', '~> 0.66', require: false + # PDF Export tests + gem 'pdf-inspector', '~> 1.2' + # brings back testing for 'assigns' and 'assert_template' extracted in rails 5 gem 'rails-controller-testing', '~> 1.0.2' @@ -275,10 +277,6 @@ group :development do gem 'spring' gem 'spring-commands-rspec' - # Gems for living styleguide - gem 'livingstyleguide', '~> 2.1.0' - gem 'sassc-rails' - gem 'colored2' # git hooks manager @@ -357,3 +355,5 @@ gemfiles.each do |file| end gem "primer_view_components", git: 'https://github.com/opf/primer_view_components', ref: '18abe4d' +gem "openproject-octicons", '~>19.6.7' +gem "openproject-octicons_helper", '~>19.6.7' diff --git a/Gemfile.lock b/Gemfile.lock index 9b18ad89451..63d44f6d9e8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -216,6 +216,7 @@ PATH GEM remote: https://rubygems.org/ specs: + Ascii85 (1.1.0) actioncable (7.0.6) actionpack (= 7.0.6) activesupport (= 7.0.6) @@ -302,14 +303,15 @@ GEM activerecord (>= 4.2) acts_as_tree (2.9.1) activerecord (>= 3.0.0) - addressable (2.8.4) + addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) + afm (0.2.2) airbrake (13.0.3) airbrake-ruby (~> 6.0) airbrake-ruby (6.2.1) rbtree3 (~> 0.6) - appsignal (3.4.8) + appsignal (3.4.10) rack ast (2.4.2) attr_required (1.0.1) @@ -318,8 +320,8 @@ GEM awesome_nested_set (3.5.0) activerecord (>= 4.0.0, < 7.1) aws-eventstream (1.2.0) - aws-partitions (1.792.0) - aws-sdk-core (3.180.0) + aws-partitions (1.796.0) + aws-sdk-core (3.180.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) @@ -370,7 +372,7 @@ GEM with_advisory_lock (>= 4.0.0) coderay (1.1.3) colored2 (3.1.2) - commonmarker (0.23.9) + commonmarker (0.23.10) compare-xml (0.66) nokogiri (~> 1.8) concurrent-ruby (1.2.2) @@ -378,6 +380,8 @@ GEM crack (0.4.5) rexml crass (1.0.6) + css_parser (1.14.0) + addressable cuprite (0.14.3) capybara (~> 3.0) ferrum (~> 0.13.0) @@ -530,11 +534,14 @@ GEM gravatar_image_tag (1.2.0) hana (1.3.7) hashdiff (1.0.1) + hashery (2.1.2) hashie (3.6.0) html-pipeline (2.14.3) activesupport (>= 2) nokogiri (>= 1.4) + htmlbeautifier (1.4.2) htmldiff (0.0.1) + htmlentities (4.3.4) http-accept (1.7.0) http-cookie (1.0.5) domain_name (~> 0.5) @@ -576,18 +583,12 @@ GEM language_server-protocol (3.17.0.3) launchy (2.5.2) addressable (~> 2.8) - lefthook (1.4.7) + lefthook (1.4.8) letter_opener (1.8.1) launchy (>= 2.2, < 3) listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - livingstyleguide (2.1.0) - minisyntax (>= 0.2.5) - redcarpet - sassc - thor - tilt lobby_boy (0.1.3) omniauth (~> 1.1) omniauth-openid-connect (>= 0.2.1) @@ -600,6 +601,18 @@ GEM loofah (2.21.3) crass (~> 1.0.2) nokogiri (>= 1.12.0) + lookbook (2.0.5) + activemodel + css_parser + htmlbeautifier (~> 1.3) + htmlentities (~> 4.3.4) + marcel (~> 1.0) + railties (>= 5.0) + redcarpet (~> 3.5) + rouge (>= 3.26, < 5.0) + view_component (>= 2.0) + yard (~> 0.9.25) + zeitwerk (~> 2.5) mail (2.8.1) mini_mime (>= 0.1.1) net-imap @@ -617,9 +630,8 @@ GEM mime-types-data (3.2023.0218.1) mini_magick (4.12.0) mini_mime (1.1.2) - mini_portile2 (2.8.2) - minisyntax (0.2.5) - minitest (5.18.1) + mini_portile2 (2.8.4) + minitest (5.19.0) msgpack (1.7.1) multi_json (1.15.0) mustermann (3.0.0) @@ -642,7 +654,7 @@ GEM mini_portile2 (~> 2.8.2) racc (~> 1.4) octicons (19.4.0) - oj (3.15.0) + oj (3.15.1) okcomputer (1.18.4) omniauth-saml (1.10.3) omniauth (~> 1.3, >= 1.3.2) @@ -661,6 +673,11 @@ GEM validate_email validate_url webfinger (~> 2.0) + openproject-octicons (19.6.7) + openproject-octicons_helper (19.6.7) + actionview + openproject-octicons (= 19.6.7) + railties openproject-token (3.0.1) activemodel os (1.1.4) @@ -675,12 +692,19 @@ GEM ast (~> 2.4.1) racc pdf-core (0.9.0) + pdf-inspector (1.3.0) + pdf-reader (>= 1.0, < 3.0.a) + pdf-reader (2.11.0) + Ascii85 (~> 1.0) + afm (~> 0.2.1) + hashery (~> 2.0) + ruby-rc4 + ttfunk pg (1.5.3) plaintext (0.3.4) activesupport (> 2.2.1) nokogiri (~> 1.10, >= 1.10.4) rubyzip (>= 1.2.0) - posix-spawn (0.3.15) prawn (2.4.0) pdf-core (~> 0.9.0) ttfunk (~> 1.7) @@ -714,14 +738,14 @@ GEM puma (>= 5.0, < 7) raabro (1.4.0) racc (1.7.1) - rack (2.2.7) + rack (2.2.8) rack-accept (0.4.5) rack (>= 0.4) rack-attack (6.6.1) rack (>= 1.0, < 3) rack-cors (2.0.1) rack (>= 2.0.0) - rack-mini-profiler (3.1.0) + rack-mini-profiler (3.1.1) rack (>= 1.2.0) rack-oauth2 (2.2.0) activesupport @@ -801,12 +825,12 @@ GEM mime-types (>= 1.16, < 4.0) netrc (~> 0.8) retriable (3.1.2) - rexml (3.2.5) + rexml (3.2.6) rinku (2.0.6) roar (1.2.0) representable (~> 3.1) rotp (6.2.2) - rouge (4.1.2) + rouge (4.1.3) rspec (3.12.0) rspec-core (~> 3.12.0) rspec-expectations (~> 3.12.0) @@ -830,7 +854,7 @@ GEM rspec-retry (0.6.2) rspec-core (> 3.3) rspec-support (3.12.1) - rubocop (1.55.0) + rubocop (1.55.1) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -851,7 +875,7 @@ GEM activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) - rubocop-rspec (2.22.0) + rubocop-rspec (2.23.0) rubocop (~> 1.33) rubocop-capybara (~> 2.17) rubocop-factory_bot (~> 2.22) @@ -862,6 +886,7 @@ GEM ruby-ole (1.2.12.2) ruby-prof (1.6.3) ruby-progressbar (1.13.0) + ruby-rc4 (0.1.5) ruby-saml (1.15.0) nokogiri (>= 1.13.10) rexml @@ -872,14 +897,6 @@ GEM sanitize (6.0.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) - sassc (2.4.0) - ffi (~> 1.9) - sassc-rails (2.1.2) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt secure_headers (6.5.0) selenium-webdriver (4.10.0) rexml (~> 3.2, >= 3.2.5) @@ -927,7 +944,6 @@ GEM test-prof (1.2.2) text-hyphen (1.5.0) thor (1.2.2) - tilt (2.2.0) timecop (0.9.6) timeout (0.4.0) trailblazer-option (0.1.2) @@ -983,7 +999,8 @@ GEM activerecord (>= 4.2) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.8) + yard (0.9.34) + zeitwerk (2.6.11) PLATFORMS ruby @@ -1056,8 +1073,8 @@ DEPENDENCIES lefthook letter_opener listen (~> 3.8.0) - livingstyleguide (~> 2.1.0) lograge (~> 0.12.0) + lookbook (~> 2.0.5) mail (= 2.8.1) matrix (~> 0.4.2) md_to_pdf! @@ -1085,6 +1102,8 @@ DEPENDENCIES openproject-job_status! openproject-ldap_groups! openproject-meeting! + openproject-octicons (~> 19.6.7) + openproject-octicons_helper (~> 19.6.7) openproject-openid_connect! openproject-recaptcha! openproject-reporting! @@ -1098,9 +1117,9 @@ DEPENDENCIES ox paper_trail (~> 12.3) parallel_tests (~> 4.0) + pdf-inspector (~> 1.2) pg (~> 1.5.0) plaintext (~> 0.3.2) - posix-spawn (~> 0.3.13) prawn (~> 2.2) primer_view_components! pry-byebug (~> 3.10.0) @@ -1138,7 +1157,6 @@ DEPENDENCIES ruby-progressbar (~> 1.13.0) rubytree (~> 2.0.0) sanitize (~> 6.0.2) - sassc-rails secure_headers (~> 6.5.0) selenium-webdriver (~> 4.0) semantic (~> 1.6.1) diff --git a/app/assets/images/icon_logo.svg b/app/assets/images/icon_logo.svg new file mode 100644 index 00000000000..8cfeda1fe4f --- /dev/null +++ b/app/assets/images/icon_logo.svg @@ -0,0 +1,56 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/assets/images/icon_logo_white.svg b/app/assets/images/icon_logo_white.svg new file mode 100644 index 00000000000..8cec02cbf05 --- /dev/null +++ b/app/assets/images/icon_logo_white.svg @@ -0,0 +1,6 @@ + + + + diff --git a/app/assets/stylesheets/styleguide.html.lsg b/app/assets/stylesheets/styleguide.html.lsg deleted file mode 100644 index 2a226583d5d..00000000000 --- a/app/assets/stylesheets/styleguide.html.lsg +++ /dev/null @@ -1 +0,0 @@ -@import ../../../frontend/src/global_styles/**/_*.lsg diff --git a/app/components/ldap_auth_sources/row_component.rb b/app/components/ldap_auth_sources/row_component.rb index 5c4e17ceb98..4eab374bf68 100644 --- a/app/components/ldap_auth_sources/row_component.rb +++ b/app/components/ldap_auth_sources/row_component.rb @@ -31,7 +31,12 @@ module LdapAuthSources class RowComponent < ::RowComponent def name - link_to model.name, edit_ldap_auth_source_path(model) + content = link_to model.name, edit_ldap_auth_source_path(model) + if model.seeded_from_env? + content += helpers.op_icon('icon icon-info2', title: I18n.t(:label_seeded_from_env_warning)) + end + + content end delegate :host, to: :model diff --git a/app/components/oauth/applications/row_component.rb b/app/components/oauth/applications/row_component.rb index 68285e00a7f..d7b3ac0f416 100644 --- a/app/components/oauth/applications/row_component.rb +++ b/app/components/oauth/applications/row_component.rb @@ -59,7 +59,7 @@ module OAuth def client_credentials if user_id = application.client_credentials_user_id - link_to_user User.find(user_id) + helpers.link_to_user User.find(user_id) else '-' end diff --git a/app/contracts/groups/delete_contract.rb b/app/contracts/groups/delete_contract.rb index 028a2656f06..536f3c28cf1 100644 --- a/app/contracts/groups/delete_contract.rb +++ b/app/contracts/groups/delete_contract.rb @@ -28,6 +28,6 @@ module Groups class DeleteContract < ::DeleteContract - delete_permission -> { user.active? && user.admin? } + delete_permission(:admin) end end diff --git a/app/models/work_package/available_custom_fields.rb b/app/contracts/roles/delete_contract.rb similarity index 88% rename from app/models/work_package/available_custom_fields.rb rename to app/contracts/roles/delete_contract.rb index c909428f814..d0a28ba3733 100644 --- a/app/models/work_package/available_custom_fields.rb +++ b/app/contracts/roles/delete_contract.rb @@ -26,8 +26,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module WorkPackage::AvailableCustomFields - def self.for(project, type) - project && type ? (project.all_work_package_custom_fields & type.custom_fields) : [] +module Roles + class DeleteContract < ::DeleteContract + delete_permission(:admin) end end diff --git a/app/controllers/admin/settings/icalendar_settings_controller.rb b/app/controllers/admin/settings/icalendar_settings_controller.rb index 843295ae1d1..df970f0fde8 100644 --- a/app/controllers/admin/settings/icalendar_settings_controller.rb +++ b/app/controllers/admin/settings/icalendar_settings_controller.rb @@ -31,7 +31,7 @@ module Admin::Settings menu_item :icalendar def default_breadcrumb - t(:label_icalendar) + t(:label_calendar_subscriptions) end end end diff --git a/app/controllers/ldap_auth_sources_controller.rb b/app/controllers/ldap_auth_sources_controller.rb index c270e689b0b..7438cee241e 100644 --- a/app/controllers/ldap_auth_sources_controller.rb +++ b/app/controllers/ldap_auth_sources_controller.rb @@ -34,24 +34,26 @@ class LdapAuthSourcesController < ApplicationController before_action :require_admin before_action :block_if_password_login_disabled + self._model_object = LdapAuthSource + before_action :find_model_object, only: %i(edit update destroy) + before_action :prevent_editing_when_seeded, only: %i(update) + def index - @auth_sources = LdapAuthSource + @ldap_auth_sources = LdapAuthSource .order(id: :asc) .page(page_param) .per_page(per_page_param) end def new - @auth_source = LdapAuthSource.new + @ldap_auth_source = LdapAuthSource.new end - def edit - @auth_source = LdapAuthSource.find(params[:id]) - end + def edit; end def create - @auth_source = LdapAuthSource.new permitted_params.ldap_auth_source - if @auth_source.save + @ldap_auth_source = LdapAuthSource.new permitted_params.ldap_auth_source + if @ldap_auth_source.save flash[:notice] = I18n.t(:notice_successful_create) redirect_to action: 'index' else @@ -60,11 +62,11 @@ class LdapAuthSourcesController < ApplicationController end def update - @auth_source = LdapAuthSource.find(params[:id]) + @ldap_auth_source = LdapAuthSource.find(params[:id]) updated = permitted_params.ldap_auth_source updated.delete :account_password if updated[:account_password].blank? - if @auth_source.update updated + if @ldap_auth_source.update updated flash[:notice] = I18n.t(:notice_successful_update) redirect_to action: 'index' else @@ -84,9 +86,9 @@ class LdapAuthSourcesController < ApplicationController end def destroy - @auth_source = LdapAuthSource.find(params[:id]) - if @auth_source.users.empty? - @auth_source.destroy + @ldap_auth_source = LdapAuthSource.find(params[:id]) + if @ldap_auth_source.users.empty? + @ldap_auth_source.destroy flash[:notice] = t(:notice_successful_delete) else @@ -97,6 +99,13 @@ class LdapAuthSourcesController < ApplicationController protected + def prevent_editing_when_seeded + if @ldap_auth_source.seeded_from_env? + flash[:warning] = I18n.t(:label_seeded_from_env_warning) + redirect_to action: :index + end + end + def default_breadcrumb if action_name == 'index' t(:label_ldap_auth_source_plural) diff --git a/app/controllers/my_controller.rb b/app/controllers/my_controller.rb index 68c532e24b8..d3da0317ff3 100644 --- a/app/controllers/my_controller.rb +++ b/app/controllers/my_controller.rb @@ -78,7 +78,26 @@ class MyController < ApplicationController end # Administer access tokens - def access_token; end + def access_token + @storage_tokens = OAuthClientToken + .preload(:oauth_client) + .joins(:oauth_client) + .where(user: @user, oauth_client: { integration_type: 'Storages::Storage' }) + end + + def delete_storage_token + token = OAuthClientToken + .preload(:oauth_client) + .joins(:oauth_client) + .where(user: @user, oauth_client: { integration_type: 'Storages::Storage' }).find_by(id: params[:id]) + + if token&.destroy + flash[:info] = I18n.t('my.access_tokens.storages.removed') + else + flash[:error] = I18n.t('my.access_tokens.storages.failed') + end + redirect_to action: :access_token + end # Configure user's in app notifications def notifications diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index 15a959a444b..331b8e3c212 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -28,7 +28,6 @@ class RolesController < ApplicationController include PaginationHelper - include Roles::NotifyMixin layout 'admin' @@ -54,7 +53,7 @@ class RolesController < ApplicationController end def create - @call = create_role + @call = Roles::CreateService.new(user: current_user).call(create_params) @role = @call.result if @call.success? @@ -80,13 +79,16 @@ class RolesController < ApplicationController end def destroy - @role = Role.find(params[:id]) - @role.destroy - flash[:notice] = I18n.t(:notice_successful_delete) - redirect_to action: 'index' - notify_changed_roles(:removed, @role) - rescue StandardError - flash[:error] = I18n.t(:error_can_not_remove_role) + service_result = Roles::DeleteService.new( + model: Role.find(params[:id]), + user: current_user + ).call + + if service_result.success? + flash[:notice] = I18n.t(:notice_successful_delete) + else + flash[:error] = I18n.t(:error_can_not_remove_role) + end redirect_to action: 'index' end @@ -148,12 +150,6 @@ class RolesController < ApplicationController end end - def create_role - Roles::CreateService - .new(user: current_user) - .call(create_params) - end - def roles_scope Role.order(Arel.sql('builtin, position')) end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index c29f484e9d1..64138919770 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -72,7 +72,7 @@ class UsersController < ApplicationController if can_show_user? @events = events - render layout: 'no_menu' + render layout: (can_manage_or_create_users? ? 'admin' : 'no_menu') else render_404 end @@ -232,14 +232,18 @@ class UsersController < ApplicationController private def can_show_user? - return true if current_user.allowed_to_globally?(:manage_user) - return true if current_user.allowed_to_globally?(:create_user) + return true if can_manage_or_create_users? return true if @user == User.current (@user.active? || @user.registered?) \ && (@memberships.present? || events.present?) end + def can_manage_or_create_users? + current_user.allowed_to_globally?(:manage_user) || + current_user.allowed_to_globally?(:create_user) + end + def events @events ||= Activities::Fetcher.new(User.current, author: @user).events(limit: 10) end @@ -304,7 +308,7 @@ class UsersController < ApplicationController end def show_local_breadcrumb - action_name != 'show' + can_manage_or_create_users? end def build_user_update_params diff --git a/app/helpers/frontend_asset_helper.rb b/app/helpers/frontend_asset_helper.rb index a19b69c3da7..664c7c88d29 100644 --- a/app/helpers/frontend_asset_helper.rb +++ b/app/helpers/frontend_asset_helper.rb @@ -54,6 +54,12 @@ module FrontendAssetHelper end end + def include_spot_assets + capture do + concat stylesheet_link_tag variable_asset_path("spot.css"), media: :all, skip_pipeline: true + end + end + private def angular_cli_asset(path) diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb index ad5571159c9..960f41fa362 100644 --- a/app/models/custom_field.rb +++ b/app/models/custom_field.rb @@ -275,7 +275,7 @@ class CustomField < ApplicationRecord end def multi_value_possible? - %w[user list].include?(field_format) && + %w[version user list].include?(field_format) && [ProjectCustomField, WorkPackageCustomField].include?(self.class) end diff --git a/app/models/journable/with_historic_attributes/loader.rb b/app/models/journable/with_historic_attributes/loader.rb index 2b21013ac2a..f4e8ca6221b 100644 --- a/app/models/journable/with_historic_attributes/loader.rb +++ b/app/models/journable/with_historic_attributes/loader.rb @@ -114,7 +114,7 @@ class Journable::WithHistoricAttributes Journal::CustomizableJournal .where(journal_id: journal_ids) .includes(:custom_field) - .index_by(&:journal_id) + .group_by(&:journal_id) end def set_custom_value_association_from_journal!(work_package:, customizable_journals:) diff --git a/app/models/journal.rb b/app/models/journal.rb index 81f24baf5a9..349474bc9ef 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -63,6 +63,7 @@ class Journal < ApplicationRecord work_package_children_changed_times work_package_related_changed_times working_days_changed + system_update ].freeze # Make sure each journaled model instance only has unique version ids diff --git a/app/models/ldap_auth_source.rb b/app/models/ldap_auth_source.rb index c0ac6a01988..9f02a86c90e 100644 --- a/app/models/ldap_auth_source.rb +++ b/app/models/ldap_auth_source.rb @@ -94,6 +94,10 @@ class LdapAuthSource < ApplicationRecord nil end + def seeded_from_env? + Setting.seed_ldap&.key?(name) + end + def account_password read_ciphered_attribute(:account_password) end @@ -136,10 +140,12 @@ class LdapAuthSource < ApplicationRecord # test the connection to the LDAP def test_connection unless authenticate_dn(account, account_password) - raise LdapAuthSource::Error, I18n.t('auth_source.ldap_error', error_message: I18n.t('auth_source.ldap_auth_failed')) + raise LdapAuthSource::Error, + I18n.t('ldap_auth_sources.ldap_error', error_message: I18n.t('ldap_auth_sources.ldap_auth_failed')) end rescue Net::LDAP::Error => e - raise LdapAuthSource::Error, I18n.t('auth_source.ldap_error', error_message: e.to_s) + raise LdapAuthSource::Error, + I18n.t('ldap_auth_sources.ldap_error', error_message: e.to_s) end def get_user_attributes_from_ldap_entry(entry) diff --git a/app/models/project.rb b/app/models/project.rb index e29dd39e37c..3733b2f8fa6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -85,8 +85,8 @@ class Project < ApplicationRecord association_foreign_key: 'custom_field_id' has_many :budgets, dependent: :destroy has_many :notification_settings, dependent: :destroy - has_many :projects_storages, dependent: :destroy, class_name: 'Storages::ProjectStorage' - has_many :storages, through: :projects_storages + has_many :project_storages, dependent: :destroy, class_name: 'Storages::ProjectStorage' + has_many :storages, through: :project_storages acts_as_customizable acts_as_searchable columns: %W(#{table_name}.name #{table_name}.identifier #{table_name}.description), @@ -213,12 +213,6 @@ class Project < ApplicationRecord Authorization.projects(permission, user) end - def reload(*args) - @all_work_package_custom_fields = nil - - super - end - # Returns a :conditions SQL string that can be used to find the issues associated with this project. # # Examples: diff --git a/app/models/query.rb b/app/models/query.rb index 8c20401bc18..34f35d01320 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -437,8 +437,7 @@ class Query < ApplicationRecord end def allowed_timestamps - return timestamps if EnterpriseToken.allows_to?(:baseline_comparison) - timestamps.select { |t| t.one_day_ago? || t.to_time >= Date.yesterday } + Timestamp.allowed(timestamps) end def valid_filter_subset! diff --git a/app/models/role.rb b/app/models/role.rb index a599c0eeec2..d1c328e8eea 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -39,7 +39,13 @@ class Role < ApplicationRecord where("#{compare} builtin = #{NON_BUILTIN}") } - before_destroy :check_deletable + before_destroy(prepend: true) do + unless deletable? + errors.add(:base, "can't be destroyed") + raise ActiveRecord::RecordNotDestroyed + end + end + has_many :workflows, dependent: :delete_all do def copy_from_role(source_role) Workflow.copy(nil, source_role, nil, proxy_association.owner) @@ -74,7 +80,7 @@ class Role < ApplicationRecord end def permissions=(perms) - not_included_yet = (perms.map(&:to_sym) - permissions).reject(&:blank?) + not_included_yet = (perms.map(&:to_sym) - permissions).compact_blank included_until_now = permissions - perms.map(&:to_sym) remove_permission!(*included_until_now) @@ -176,6 +182,10 @@ class Role < ApplicationRecord .first end + def deletable? + members.none? && !builtin? + end + private def allowed_permissions @@ -188,11 +198,6 @@ class Role < ApplicationRecord end end - def check_deletable - raise "Can't delete role" if members.any? - raise "Can't delete builtin role" if builtin? - end - def add_permission(permission) if persisted? role_permissions.create(permission:) diff --git a/app/models/timestamp.rb b/app/models/timestamp.rb index dd5f57ac388..0bdfada5b68 100644 --- a/app/models/timestamp.rb +++ b/app/models/timestamp.rb @@ -102,6 +102,12 @@ class Timestamp def now new(ActiveSupport::Duration.build(0).iso8601) end + + def allowed(timestamps) + return timestamps if EnterpriseToken.allows_to?(:baseline_comparison) + + timestamps.select { |t| t.one_day_ago? || t.to_time >= Date.yesterday } + end end def initialize(arg = Timestamp.now.to_s) diff --git a/app/models/user.rb b/app/models/user.rb index ba5e0440919..f94cd4a2355 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -326,8 +326,8 @@ class User < Principal # If +update_legacy+ is set, will automatically save legacy passwords using the current # format. def check_password?(clear_password, update_legacy: true) - if ldap_auth_source_id.present? - auth_source.authenticate(login, clear_password) + if ldap_auth_source.present? + ldap_auth_source.authenticate(login, clear_password) else return false if current_password.nil? diff --git a/app/models/work_package.rb b/app/models/work_package.rb index bb119391d0a..cc48970983b 100644 --- a/app/models/work_package.rb +++ b/app/models/work_package.rb @@ -449,9 +449,47 @@ class WorkPackage < ApplicationRecord # Overrides Redmine::Acts::Customizable::ClassMethods#available_custom_fields def self.available_custom_fields(work_package) - WorkPackage::AvailableCustomFields.for(work_package.project, work_package.type) + if work_package.project_id && work_package.type_id + RequestStore.fetch(available_custom_field_key(work_package)) do + available_custom_fields_from_db([work_package]) + end + else + [] + end end + def self.preload_available_custom_fields(work_packages) + custom_fields = available_custom_fields_from_db(work_packages) + .select('projects.id project_id', + 'types.id type_id', + 'custom_fields.*') + + work_packages.each do |work_package| + RequestStore.store[available_custom_field_key(work_package)] = custom_fields + .select do |cf| + ((cf.project_id == work_package.project_id) || cf.is_for_all?) && cf.type_id == work_package.type_id + end + end + end + + def self.available_custom_fields_from_db(work_packages) + WorkPackageCustomField + .left_joins(:projects, :types) + .where(projects: { id: work_packages.map(&:project_id).uniq }, + types: { id: work_packages.map(&:type_id).uniq }) + .or(WorkPackageCustomField + .left_joins(:projects, :types) + .references(:projects, :types) + .where(is_for_all: true) + .where(types: { id: work_packages.map(&:type_id).uniq })) + end + private_class_method :available_custom_fields_from_db + + def self.available_custom_field_key(work_package) + :"#work_package_custom_fields_#{work_package.project_id}_#{work_package.type_id}" + end + private_class_method :available_custom_field_key + def custom_field_cache_key [project_id, type_id] end diff --git a/app/models/work_package/pdf_export/markdown.rb b/app/models/work_package/pdf_export/markdown.rb index 1b781a376c8..842ca67c38c 100644 --- a/app/models/work_package/pdf_export/markdown.rb +++ b/app/models/work_package/pdf_export/markdown.rb @@ -75,27 +75,19 @@ module WorkPackage::PDFExport::Markdown end def handle_unknown_inline_html_tag(tag, node, opts) - result = [] - case tag.name - when 'mention' - return [handle_mention_html_tag(tag, node, opts), opts] - when 'span' - text = tag.text - result.push(text_hash(text, opts)) if text.present? - else - result.push(text_hash(tag.to_s, opts)) - end + result = if tag.name == 'mention' + handle_mention_html_tag(tag, node, opts) + else + # unknown/unsupported html tags eg. hi are ignored + # but scanned for supported or text children + data_inlinehtml_tag(tag, node, opts) + end [result, opts] end - def handle_unknown_html_tag(tag, node, opts) - case tag.name - when 'div', 'p' - # nop, but scan children [true, ...] - else - draw_formatted_text([text_hash(tag.to_s, opts)], opts, node) - return [false, opts] - end + def handle_unknown_html_tag(_tag, _node, opts) + # unknown/unsupported html tags eg. hi are ignored + # but scanned for supported or text children [true, ...] [true, opts] end diff --git a/app/seeders/env_data/ldap_seeder.rb b/app/seeders/env_data/ldap_seeder.rb new file mode 100644 index 00000000000..b9dabd6aa95 --- /dev/null +++ b/app/seeders/env_data/ldap_seeder.rb @@ -0,0 +1,119 @@ +#-- copyright + +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +module EnvData + class LdapSeeder < Seeder + def seed_data! + print_status ' ↳ Creating LDAP connection' do + Setting.seed_ldap.each do |name, options| + ldap = LdapAuthSource.find_or_initialize_by(name:) + + print_ldap_status(ldap) + upsert_settings(ldap, options) + update_filters(ldap, options['groupfilter']) + end + end + + print_status ' ↳ Synchronizing LDAP connections' do + LdapGroups::SynchronizationJob.perform_now + end + end + + def applicable? + Setting.seed_ldap.present? + end + + private + + # rubocop:disable Metrics/AbcSize + def upsert_settings(ldap, options) + ldap.host = options['host'] + ldap.port = options['port'] + + ldap.tls_mode = options['security'] + ldap.verify_peer = ActiveRecord::Type::Boolean.new.deserialize options.fetch('tls_verify', true) + ldap.onthefly_register = ActiveRecord::Type::Boolean.new.deserialize options.fetch('sync_users', false) + ldap.tls_certificate_string = options['tls_certificate'].presence + + ldap.filter_string = options['filter'].presence + ldap.base_dn = options['basedn'] + ldap.account = options['binduser'] + ldap.account_password = options['bindpassword'] + + ldap.attr_login = options['login_mapping'].presence + ldap.attr_firstname = options['firstname_mapping'].presence + ldap.attr_lastname = options['lastname_mapping'].presence + ldap.attr_mail = options['mail_mapping'].presence + ldap.attr_admin = options['admin_mapping'].presence + + ldap.save! + end + # rubocop:enable Metrics/AbcSize + + def update_filters(ldap, filters) + return if filters.blank? && !LdapGroups::SynchronizedFilter.exists?(ldap_auth_source: ldap) + + upsert_existing_filters(ldap, filters) if filters.present? + remove_not_found_filters(ldap, filters&.keys || []) + end + + def upsert_existing_filters(ldap, filters) + filters.each do |name, options| + filter = ::LdapGroups::SynchronizedFilter.find_or_initialize_by(ldap_auth_source: ldap, name:) + print_ldap_status(filter) + + filter.group_name_attribute = options.fetch('group_attribute', 'dn') + filter.sync_users = ActiveRecord::Type::Boolean.new.deserialize options.fetch('sync_users', false) + filter.filter_string = options['filter'] + filter.base_dn = options['base'] + + filter.save! + end + end + + def remove_not_found_filters(ldap, names) + not_found = ::LdapGroups::SynchronizedFilter + .where(ldap_auth_source: ldap) + .where.not(name: names) + + return unless not_found.exists? + + not_found_names = not_found.pluck(:name).join(', ') + print_status " - Removing LDAP filter #{not_found_names} no longer present in ENV" + + not_found.destroy_all + end + + def print_ldap_status(object) + if object.new_record? + print_status " - Creating new #{object.model_name.human} #{object.name} from ENV" + else + print_status " - Updating existing #{object.model_name.human} #{object.name} from ENV" + end + end + end +end diff --git a/app/services/roles/notify_mixin.rb b/app/seeders/env_data_seeder.rb similarity index 87% rename from app/services/roles/notify_mixin.rb rename to app/seeders/env_data_seeder.rb index 9ee13bac1f1..d4ecf0519ba 100644 --- a/app/services/roles/notify_mixin.rb +++ b/app/seeders/env_data_seeder.rb @@ -24,12 +24,14 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # See COPYRIGHT and LICENSE files for more details. -#++ +class EnvDataSeeder < CompositeSeeder + def data_seeder_classes + [ + EnvData::LdapSeeder + ] + end -module Roles::NotifyMixin - private - - def notify_changed_roles(action, changed_role) - OpenProject::Notifications.send(:roles_changed, action:, role: changed_role) + def namespace + 'EnvData' end end diff --git a/app/seeders/root_seeder.rb b/app/seeders/root_seeder.rb index 10a29f855b8..d8baf468235 100644 --- a/app/seeders/root_seeder.rb +++ b/app/seeders/root_seeder.rb @@ -63,6 +63,7 @@ class RootSeeder < Seeder seed_demo_data seed_development_data if seed_development_data? seed_plugins_data + seed_env_data end end @@ -140,6 +141,11 @@ class RootSeeder < Seeder DemoDataSeeder.new(seed_data).seed! end + def seed_env_data + print_status '*** Seeding data from environment variables' + EnvDataSeeder.new(seed_data).seed! + end + def seed_development_data print_status '*** Seeding development data' require 'factory_bot' diff --git a/app/services/journals/create_service.rb b/app/services/journals/create_service.rb index 1b98db82c01..d32a96f3dbf 100644 --- a/app/services/journals/create_service.rb +++ b/app/services/journals/create_service.rb @@ -428,13 +428,15 @@ module Journals storages_file_links_journals ( journal_id, file_link_id, - link_name + link_name, + storage_name ) SELECT #{id_from_inserted_journal_sql}, file_links.id, - file_links.origin_name - FROM file_links + file_links.origin_name, + storages.name + FROM file_links left join storages ON file_links.storage_id = storages.id WHERE #{only_if_created_sql} AND file_links.container_id = :journable_id diff --git a/app/services/oauth_clients/connection_manager.rb b/app/services/oauth_clients/connection_manager.rb index e83171c7586..cee446293d0 100644 --- a/app/services/oauth_clients/connection_manager.rb +++ b/app/services/oauth_clients/connection_manager.rb @@ -118,6 +118,10 @@ module OAuthClients expires_in: rack_access_token.raw_attributes[:expires_in], scope: rack_access_token.scope ) + OpenProject::Notifications.send( + OpenProject::Events::OAUTH_CLIENT_TOKEN_CREATED, + integration_type: @oauth_client.integration_type + ) end ServiceResult.success(result: oauth_client_token) diff --git a/app/services/projects/delete_service.rb b/app/services/projects/delete_service.rb index 9ad03d8093a..24c515891c8 100644 --- a/app/services/projects/delete_service.rb +++ b/app/services/projects/delete_service.rb @@ -53,6 +53,7 @@ module Projects delete_all_members destroy_all_work_packages + destroy_all_storages super end @@ -76,6 +77,12 @@ module Projects end end + def destroy_all_storages + model.project_storages.map do |project_storage| + Storages::ProjectStorages::DeleteService.new(user:, model: project_storage).call + end + end + def notify(success) if success ProjectMailer.delete_project_completed(model, user:, dependent_projects:).deliver_now diff --git a/app/services/roles/create_service.rb b/app/services/roles/create_service.rb index 92f166656ca..e4a3fb29c51 100644 --- a/app/services/roles/create_service.rb +++ b/app/services/roles/create_service.rb @@ -27,8 +27,6 @@ #++ class Roles::CreateService < BaseServices::Create - include Roles::NotifyMixin - private def perform(params) @@ -38,8 +36,6 @@ class Roles::CreateService < BaseServices::Create if super_call.success? copy_workflows(copy_workflow_id, super_call.result) - - notify_changed_roles(:added, super_call.result) end super_call diff --git a/app/services/roles/delete_service.rb b/app/services/roles/delete_service.rb new file mode 100644 index 00000000000..12bbef8dfc8 --- /dev/null +++ b/app/services/roles/delete_service.rb @@ -0,0 +1,46 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +class Roles::DeleteService < BaseServices::Delete + def persist(service_result) + # after destroy permissions can not be reached + @permissions = model.permissions + super(service_result) + end + + protected + + def after_perform(service_call) + super(service_call).tap do |_call| + ::OpenProject::Notifications.send( + ::OpenProject::Events::ROLE_DESTROYED, + permissions: @permissions + ) + end + end +end diff --git a/app/services/roles/update_service.rb b/app/services/roles/update_service.rb index 6e0c08c9868..cb67f717c20 100644 --- a/app/services/roles/update_service.rb +++ b/app/services/roles/update_service.rb @@ -27,11 +27,20 @@ #++ class Roles::UpdateService < BaseServices::Update - include Roles::NotifyMixin - private - def after_safe - notify_changed_roles(:updated, model) + def before_perform(params, service_call) + @permissions_old = service_call.result.permissions + super + end + + def after_perform(call) + permissions_new = call.result.permissions + permissions_diff = (@permissions_old - permissions_new) | (permissions_new - @permissions_old) + OpenProject::Notifications.send( + OpenProject::Events::ROLE_UPDATED, + permissions_diff: + ) + call end end diff --git a/app/views/admin/settings/icalendar_settings/show.html.erb b/app/views/admin/settings/icalendar_settings/show.html.erb index 156775e2393..f463d7ee223 100644 --- a/app/views/admin/settings/icalendar_settings/show.html.erb +++ b/app/views/admin/settings/icalendar_settings/show.html.erb @@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -<%= toolbar title: t(:label_icalendar) %> +<%= toolbar title: t(:label_calendar_subscriptions) %> <%= styled_form_tag( admin_settings_icalendar_path, diff --git a/app/views/custom_styles/show.html.erb b/app/views/custom_styles/show.html.erb index dacc3e224f9..af9c771a856 100644 --- a/app/views/custom_styles/show.html.erb +++ b/app/views/custom_styles/show.html.erb @@ -230,7 +230,7 @@ See COPYRIGHT and LICENSE files for more details. var computedStyle = getComputedStyle(document.documentElement); document - .querySelectorAlt('.design-color--variable-input') + .querySelectorAll('.design-color--variable-input') .forEach(function(el) { if (!el.value || el.value === '') { el.placeholder = computedStyle.getPropertyValue('--' + el.dataset.variableName).trim(); diff --git a/app/views/homescreen/blocks/_upsale.html.erb b/app/views/homescreen/blocks/_upsale.html.erb index bade7a0d63c..126e5117bb3 100644 --- a/app/views/homescreen/blocks/_upsale.html.erb +++ b/app/views/homescreen/blocks/_upsale.html.erb @@ -1,24 +1,21 @@
<%= image_tag "enterprise-add-on.svg", class: "widget-box--blocks--upsale-image" %> -
+
- <%= spot_icon('enterprise-addons') %> <%= t('homescreen.blocks.upsale.title') %> +
+

+ <%= t('js.admin.enterprise.upsale.text') %> +

+ +

+ <%= t('js.admin.enterprise.upsale.become_hero') %> <%= t('js.admin.enterprise.upsale.you_contribute') %> +

-

- <%= t('js.admin.enterprise.upsale.text') %> -

- -

- <%= t('js.admin.enterprise.upsale.become_hero') %> <%= t('js.admin.enterprise.upsale.you_contribute') %> -

- -

- <%= t('js.admin.enterprise.upsale.confidence') %> -

- -
+

+ <%= t('js.admin.enterprise.upsale.confidence') %> +

@@ -28,8 +25,9 @@ aria: {label: t('homescreen.blocks.upsale.more_info')}, target: '_blank', title: t('homescreen.blocks.upsale.more_info')} do %> + <%= spot_icon('external-link') %> <%= t('homescreen.blocks.upsale.more_info') %> - <%= spot_icon('button--icon icon-external-link') %> + <% end %> <%= link_to(OpenProject::Static::Links.links[:pricing][:href], diff --git a/app/views/layouts/component_preview.html.erb b/app/views/layouts/component_preview.html.erb new file mode 100644 index 00000000000..82fc214be44 --- /dev/null +++ b/app/views/layouts/component_preview.html.erb @@ -0,0 +1,46 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + + + <%= include_frontend_assets %> + +<% + styles = [] + min_height = params[:lookbook][:display][:min_height] + styles << "min-height: #{min_height}" if min_height.present? +%> + +<%= content_tag :body, + class: 'viewcomponent-preview', + style: styles.join(";") do %> +
+ <%= yield %> +
+<% end %> + diff --git a/app/views/layouts/styleguide/styleguide.layout.html.erb b/app/views/layouts/styleguide/styleguide.layout.html.erb deleted file mode 100644 index bd344cd2c4e..00000000000 --- a/app/views/layouts/styleguide/styleguide.layout.html.erb +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - - - - Living Style Guide for OpenProject - - - - - - - - - -
-
-

Living Style Guide

-
-
- - - -
-

 

-
-
- <%= html %> -
- - - diff --git a/app/views/ldap_auth_sources/_form.html.erb b/app/views/ldap_auth_sources/_form.html.erb index 805911feee2..419754e1cc3 100644 --- a/app/views/ldap_auth_sources/_form.html.erb +++ b/app/views/ldap_auth_sources/_form.html.erb @@ -34,7 +34,7 @@ See COPYRIGHT and LICENSE files for more details. <%= error_messages_for :ldap_auth_source %> -<% if @auth_source.new_record? %> +<% if @ldap_auth_source.new_record? %>
@@ -43,7 +43,17 @@ See COPYRIGHT and LICENSE files for more details.
<% end %> -
+<% if @ldap_auth_source.seeded_from_env? %> +
+
+ <%= t(:label_seeded_from_env_warning) %> +
+ <%= link_to t('ldap_auth_sources.back_to_index'), { action: :index } %> +
+
+<% end %> + +<%= content_tag :fieldset, class: 'form--fieldset', disabled: @ldap_auth_source.seeded_from_env? do %>
<%= f.text_field 'name', required: true, container_class: '-middle' %> @@ -57,86 +67,86 @@ See COPYRIGHT and LICENSE files for more details.
<%= f.text_field 'port', required: true, size: 6, container_class: '-xslim' %>
+<% end %> -
- <%= t('ldap_auth_sources.connection_encryption') %> -

- <%= t 'ldap_auth_sources.tls_mode.section_more_info_link_html', - link: OpenProject::Static::Links[:ldap_encryption_documentation][:href] %> +<%= content_tag :fieldset, class: 'form--fieldset', disabled: @ldap_auth_source.seeded_from_env? do %> + <%= t('ldap_auth_sources.connection_encryption') %> +

+ <%= t 'ldap_auth_sources.tls_mode.section_more_info_link_html', + link: OpenProject::Static::Links[:ldap_encryption_documentation][:href] %> +

+
+ <%= f.radio_button :tls_mode, + 'start_tls', + label: t('ldap_auth_sources.tls_mode.start_tls'), + container_class: '-wide' %> +

+ <%= t('ldap_auth_sources.tls_mode.start_tls_description') %>

-
- <%= f.radio_button :tls_mode, - 'start_tls', - label: t('ldap_auth_sources.tls_mode.start_tls'), - container_class: '-wide' %> -

- <%= t('ldap_auth_sources.tls_mode.start_tls_description') %> -

+
+
+ <%= f.radio_button :tls_mode, + 'simple_tls', + label: t('ldap_auth_sources.tls_mode.simple_tls'), + container_class: '-wide' %> +

+ <%= t('ldap_auth_sources.tls_mode.simple_tls_description') %> +

+
+
+ <%= f.radio_button :tls_mode, + 'plain_ldap', + label: t('ldap_auth_sources.tls_mode.plain'), + container_class: '-wide' %> +

+ <%= t('ldap_auth_sources.tls_mode.plain_description') %> +

+
+<% end %> +<%= content_tag :fieldset, class: 'form--fieldset', disabled: @ldap_auth_source.seeded_from_env? do %> + <%= t('ldap_auth_sources.connection_encryption') %> +
+ <%= f.check_box :verify_peer, + label: t('ldap_auth_sources.tls_options.verify_peer'), + container_class: '-wide' %> +

+ <%= t('ldap_auth_sources.tls_options.verify_peer_description_html') %> +

+
+
+ <%= f.text_area :tls_certificate_string, + label: LdapAuthSource.human_attribute_name(:tls_certificate_string), + placeholder: "-----BEGIN CERTIFICATE-----\n ..... \n-----END CERTIFICATE-----", + rows: 10, + container_class: '-wide' %> +
+

<%= t('ldap_auth_sources.tls_options.tls_certificate_description') %>

-
- <%= f.radio_button :tls_mode, - 'simple_tls', - label: t('ldap_auth_sources.tls_mode.simple_tls'), - container_class: '-wide' %> -

- <%= t('ldap_auth_sources.tls_mode.simple_tls_description') %> -

-
-
- <%= f.radio_button :tls_mode, - 'plain_ldap', - label: t('ldap_auth_sources.tls_mode.plain'), - container_class: '-wide' %> -

- <%= t('ldap_auth_sources.tls_mode.plain_description') %> -

-
-
-
- <%= t('ldap_auth_sources.connection_encryption') %> -
- <%= f.check_box :verify_peer, - label: t('ldap_auth_sources.tls_options.verify_peer'), - container_class: '-wide' %> -

- <%= t('ldap_auth_sources.tls_options.verify_peer_description_html') %> -

-
-
- <%= f.text_area :tls_certificate_string, - label: LdapAuthSource.human_attribute_name(:tls_certificate_string), - placeholder: "-----BEGIN CERTIFICATE-----\n ..... \n-----END CERTIFICATE-----", - rows: 10, - container_class: '-wide' %> -
-

<%= t('ldap_auth_sources.tls_options.tls_certificate_description') %>

-
-
-
-
- <%= t('ldap_auth_sources.system_account') %> -

<%= t 'ldap_auth_sources.system_account_legend' %>

+
+<% end %> +<%= content_tag :fieldset, class: 'form--fieldset', disabled: @ldap_auth_source.seeded_from_env? do %> + <%= t('ldap_auth_sources.system_account') %> +

<%= t 'ldap_auth_sources.system_account_legend' %>

-
- <%= f.text_field 'account', container_class: '-middle' %> - +
+ <%= f.text_field 'account', container_class: '-middle' %> + <%= t('ldap_auth_sources.attribute_texts.system_user_dn_html') %> -
-
- <%= f.password_field 'account_password', - label: LdapAuthSource.human_attribute_name(:password), - placeholder: ((@auth_source.new_record? || @auth_source.account_password.blank?) ? '' : ('●' * 15)), - autocomplete: 'off', - container_class: '-middle' %> - +
+
+ <%= f.password_field 'account_password', + label: LdapAuthSource.human_attribute_name(:password), + placeholder: ((@ldap_auth_source.new_record? || @ldap_auth_source.account_password.blank?) ? '' : ('●' * 15)), + autocomplete: 'off', + container_class: '-middle' %> + <%= t('ldap_auth_sources.attribute_texts.system_user_password') %> -
- - +
+<% end %> -
+<%= content_tag :fieldset, class: 'form--fieldset', disabled: @ldap_auth_source.seeded_from_env? do %> <%= t('ldap_auth_sources.ldap_details') %>
<%= f.text_field :base_dn, size: 60, container_class: '-wide' %> @@ -160,9 +170,9 @@ See COPYRIGHT and LICENSE files for more details.

<%= t('ldap_auth_sources.attribute_texts.onthefly_register') %>

-
+<% end %> -
+<%= content_tag :fieldset, class: 'form--fieldset', disabled: @ldap_auth_source.seeded_from_env? do %> <%= t('ldap_auth_sources.user_settings') %>

<%= t 'ldap_auth_sources.user_settings_legend' %>

@@ -213,4 +223,4 @@ See COPYRIGHT and LICENSE files for more details. <%= t('ldap_auth_sources.attribute_texts.admin_map_html') %> -
+<% end %> diff --git a/app/views/ldap_auth_sources/edit.html.erb b/app/views/ldap_auth_sources/edit.html.erb index 0e499e6f041..a7cba1c6e01 100644 --- a/app/views/ldap_auth_sources/edit.html.erb +++ b/app/views/ldap_auth_sources/edit.html.erb @@ -27,11 +27,13 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -<% html_title t(:label_administration), "#{t(:label_edit)} #{t(:label_ldap_auth_source)} #{@auth_source.name}" %> -<% local_assigns[:additional_breadcrumb] = @auth_source.name %> +<% html_title t(:label_administration), "#{t(:label_edit)} #{t(:label_ldap_auth_source)} #{@ldap_auth_source.name}" %> +<% local_assigns[:additional_breadcrumb] = @ldap_auth_source.name %> <%= toolbar title: "#{t(:label_ldap_auth_source)}" %> -<%= labelled_tabular_form_for @auth_source, as: :ldap_auth_source do |f| %> +<%= labelled_tabular_form_for @ldap_auth_source do |f| %> <%= render partial: 'form', locals: { f: f } %> - <%= styled_button_tag t(:button_save), class: '-highlight -with-icon icon-checkmark' %> + <% unless @ldap_auth_source.seeded_from_env? %> + <%= styled_button_tag t(:button_save), class: '-highlight -with-icon icon-checkmark' %> + <% end %> <% end %> diff --git a/app/views/ldap_auth_sources/index.html.erb b/app/views/ldap_auth_sources/index.html.erb index 403f9a37628..568185e1971 100644 --- a/app/views/ldap_auth_sources/index.html.erb +++ b/app/views/ldap_auth_sources/index.html.erb @@ -40,4 +40,4 @@ See COPYRIGHT and LICENSE files for more details. <% end %> -<%= render ::LdapAuthSources::TableComponent.new(rows: @auth_sources) %> +<%= render ::LdapAuthSources::TableComponent.new(rows: @ldap_auth_sources) %> diff --git a/app/views/ldap_auth_sources/new.html.erb b/app/views/ldap_auth_sources/new.html.erb index 741e16bf167..06862937c43 100644 --- a/app/views/ldap_auth_sources/new.html.erb +++ b/app/views/ldap_auth_sources/new.html.erb @@ -31,7 +31,7 @@ See COPYRIGHT and LICENSE files for more details. <% local_assigns[:additional_breadcrumb] = t(:label_ldap_auth_source_new) %> <%= toolbar title: "#{t(:label_ldap_auth_source_new)}" %> -<%= labelled_tabular_form_for @auth_source, as: :ldap_auth_source do |f| %> +<%= labelled_tabular_form_for @ldap_auth_source, as: :ldap_auth_source do |f| %> <%= render partial: 'form', locals: { f: f } %> <%= styled_button_tag t(:button_create), class: '-highlight -with-icon icon-checkmark' %> <% end %> diff --git a/app/views/my/access_token.html.erb b/app/views/my/access_token.html.erb index 6b777d72257..226f212728f 100644 --- a/app/views/my/access_token.html.erb +++ b/app/views/my/access_token.html.erb @@ -46,6 +46,9 @@ See COPYRIGHT and LICENSE files for more details. <%= render partial: 'my/access_token_partials/rss_tokens_section', locals: { rss_token: @user.rss_token } %> + <%= render partial: 'my/access_token_partials/storage_tokens_section', + locals: { storage_tokens: @storage_tokens } + %>
diff --git a/app/views/my/access_token_partials/_oauth_tokens_section.html.erb b/app/views/my/access_token_partials/_oauth_tokens_section.html.erb index 9d45c581275..1b8cb30840b 100644 --- a/app/views/my/access_token_partials/_oauth_tokens_section.html.erb +++ b/app/views/my/access_token_partials/_oauth_tokens_section.html.erb @@ -85,5 +85,3 @@ See COPYRIGHT and LICENSE files for more details. <% end %> - - diff --git a/app/views/my/access_token_partials/_storage_tokens_section.html.erb b/app/views/my/access_token_partials/_storage_tokens_section.html.erb new file mode 100644 index 00000000000..1edb509c75f --- /dev/null +++ b/app/views/my/access_token_partials/_storage_tokens_section.html.erb @@ -0,0 +1,83 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> +
+
+
+

<%= t("my_account.access_tokens.storages.title") %>

+
+
+
+

+ <%= t("my_account.access_tokens.storages.text_hint") %> +

+ <% if @storage_tokens.any? %> +
+
+
+ <%= render partial: "my/access_token_partials/token_table_header", + locals: { + column_headers: [ + t('attributes.name'), + User.human_attribute_name(:created_at), + t('my_account.access_tokens.headers.expiration') + ] + } + %> + + <% storage_tokens.each do |token| %> + + + + + + + <% end %> + +
+ <%= token.oauth_client.integration.name %> + + <%= format_time(token.created_at) %> + + <%= format_time(token.created_at + token.expires_in.seconds) %> + + <%= link_to "", + storage_token_delete_path(token.id), + data: { confirm: t('my_account.access_tokens.storages.revoke_token', storage: token.oauth_client.integration.name), + qa_selector: "storages-token-row-#{token.id}-revoke" }, + method: :delete, + class: 'icon icon-delete' %> +
+
+
+ <% else %> + + <%= t('my_account.access_tokens.storages.empty_text_hint') %> + <% end %> + + diff --git a/app/views/users/new.html.erb b/app/views/users/new.html.erb index 2660681a34c..9ea2395ce1c 100644 --- a/app/views/users/new.html.erb +++ b/app/views/users/new.html.erb @@ -43,7 +43,7 @@ See COPYRIGHT and LICENSE files for more details. 'admin--users-password-auth-selected-value': @user.ldap_auth_source_id.blank?, }, as: :user do |f| %> - <%= render partial: 'simple_form', locals: { f: f, auth_sources: @auth_sources, user: @user } %> + <%= render partial: 'simple_form', locals: { f: f, auth_sources: @ldap_auth_sources, user: @user } %>

<%= styled_button_tag t(:button_create), class: '-highlight -with-icon icon-checkmark' %> diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index fd95abc092d..19d1534ff20 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -29,8 +29,9 @@ See COPYRIGHT and LICENSE files for more details. <% content_for :header_tags do %> <%= call_hook :users_show_head %> <% end %> +<% local_assigns[:additional_breadcrumb] = @user.name %> <% html_title t(:label_administration), t(:label_user_plural) -%> -<%= toolbar title: "#{avatar @user} #{h(@user.name)}".html_safe do %> +<%= breadcrumb_toolbar "#{avatar @user} #{h(@user.name)}".html_safe do %> <% if User.current.allowed_to_globally?(:manage_user) %>

  • <%= link_to edit_user_path(@user), class: 'button', accesskey: accesskey(:edit) do %> diff --git a/config/constants/settings/definition.rb b/config/constants/settings/definition.rb index 63e355b3fed..07fd90daa37 100644 --- a/config/constants/settings/definition.rb +++ b/config/constants/settings/definition.rb @@ -590,6 +590,12 @@ module Settings login_required: { default: false }, + lookbook_enabled: { + description: 'Enable the Lookbook component documentation tool. Discouraged for production environments.', + default: -> { Rails.env.development? }, + format: :boolean, + writable: false + }, lost_password: { description: 'Activate or deactivate lost password form', default: true @@ -823,6 +829,12 @@ module Settings default: true, writable: false }, + seed_ldap: { + description: 'Provide an LDAP connection and sync settings through ENV', + writable: false, + default: nil, + format: :hash + }, self_registration: { default: 2 }, diff --git a/config/initializers/lookbook.rb b/config/initializers/lookbook.rb new file mode 100644 index 00000000000..4fd71c55862 --- /dev/null +++ b/config/initializers/lookbook.rb @@ -0,0 +1,47 @@ +OpenProject::Application.configure do + next unless OpenProject::Configuration.lookbook_enabled? + + # Re-define snapshot to avoid warnings + YARD::Tags::Library.define_tag("Snapshot preview (unused)", :snapshot) + config.lookbook.project_name = "OpenProject Lookbook" + config.lookbook.project_logo = Rails.root.join('app/assets/images/icon_logo_white.svg').read + config.lookbook.ui_favicon = Rails.root.join('app/assets/images/icon_logo.svg').read + config.lookbook.page_paths = [Rails.root.join("spec/components/docs/").to_s] + # Show notes first, all other panels next + config.lookbook.component_paths << Primer::ViewComponents::Engine.root.join("app", "components").to_s + config.view_component.preview_paths << Primer::ViewComponents::Engine.root.join("previews").to_s + config.lookbook.preview_inspector.drawer_panels = [:notes, "*"] + config.lookbook.ui_theme = "blue" + + SecureHeaders::Configuration.named_append(:lookbook) do + { + script_src: %w('unsafe-eval' 'unsafe-inline') # rubocop:disable Lint/PercentStringArray + } + end + + # rubocop:disable Lint/ConstantDefinitionInBlock + module LookbookCspExtender + extend ActiveSupport::Concern + + included do + before_action do + use_content_security_policy_named_append :lookbook + end + end + end + # rubocop:enable Lint/ConstantDefinitionInBlock + + Rails.application.reloader.to_prepare do + [ + Lookbook::ApplicationController, + Lookbook::PreviewController, + Lookbook::PreviewsController, + Lookbook::PageController, + Lookbook::PagesController, + Lookbook::InspectorController, + Lookbook::EmbedsController + ].each do |controller| + controller.include LookbookCspExtender + end + end +end diff --git a/config/initializers/menus.rb b/config/initializers/menus.rb index c96eb482bed..9493b404618 100644 --- a/config/initializers/menus.rb +++ b/config/initializers/menus.rb @@ -373,7 +373,7 @@ Redmine::MenuManager.map :admin_menu do |menu| menu.push :icalendar, { controller: '/admin/settings/icalendar_settings', action: :show }, if: Proc.new { User.current.admin? }, - caption: :label_icalendar, + caption: :label_calendar_subscriptions, parent: :calendars_and_dates menu.push :settings, diff --git a/config/initializers/subscribe_listeners.rb b/config/initializers/subscribe_listeners.rb index 9f972489475..0a5b68859b8 100644 --- a/config/initializers/subscribe_listeners.rb +++ b/config/initializers/subscribe_listeners.rb @@ -51,7 +51,7 @@ Rails.application.config.after_initialize do payload[:watcher_setter]) end - OpenProject::Notifications.subscribe(OpenProject::Events::WATCHER_REMOVED) do |payload| + OpenProject::Notifications.subscribe(OpenProject::Events::WATCHER_DESTROYED) do |payload| Mails::WatcherRemovedJob .perform_later(payload[:watcher].attributes, payload[:watcher_remover]) diff --git a/config/initializers/view_component.rb b/config/initializers/view_component.rb index 087ebd47e6e..fc77b9d988f 100644 --- a/config/initializers/view_component.rb +++ b/config/initializers/view_component.rb @@ -1,5 +1,7 @@ OpenProject::Application.configure do config.view_component.generate.preview_path = Rails.root.join("spec/components/previews").to_s config.view_component.preview_paths << Rails.root.join("spec/components/previews").to_s + config.view_component.generate.preview = true + config.view_component.default_preview_layout = "component_preview" end diff --git a/config/locales/crowdin/af.yml b/config/locales/crowdin/af.yml index 9858706e338..71fbbd1c6b9 100644 --- a/config/locales/crowdin/af.yml +++ b/config/locales/crowdin/af.yml @@ -99,15 +99,14 @@ af: edit: "Edit help text for %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: There are currently no authentication modes. - no_results_content_text: Create a new authentication mode background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | This LDAP form requires technical knowledge of your LDAP / Active Directory setup.
    @@ -449,16 +448,16 @@ af: attribute_help_text: attribute_name: 'Kenmerk' help_text: 'Help text' - auth_source: - account: "Rekening" - attr_firstname: "Eerstenaam kenmerk" - attr_lastname: "Van kenmerk" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" attr_login: "Username attribute" - attr_mail: "Epos kenmerk" - base_dn: "Basis (Onderskeidende Naam) ON" - host: "Gasheer" + attr_mail: "Email attribute" + base_dn: "Base DN" + host: "Host" onthefly: "Automatic user creation" - port: "Poort" + port: "Port" tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "Repository" @@ -557,7 +556,7 @@ af: color: "Kleur" user: admin: "Administreerder" - auth_source: "Bekragtigingsmodus" + ldap_auth_source: "LDAP connection" current_password: "Current password" force_password_change: "Dwing wagwoord veranderering af op die volgende aantekening" language: "Taal" @@ -678,7 +677,7 @@ af: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "is the wrong length (should be %{count} characters)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -900,7 +899,7 @@ af: description: "'Wagwoord bevestiging' moet ooreenstem met die insette in die 'Nuwe wagwoord' gebied." status: invalid_on_create: "is not a valid status for new users." - auth_source: + ldap_auth_source: error_not_found: "not found" member: principal_blank: "Kies asseblief ten minste een gebruiker of groep." @@ -1436,6 +1435,7 @@ af: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1450,6 +1450,8 @@ af: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Configuration guide' get_in_touch: "You have questions? Get in touch with us." @@ -1494,6 +1496,13 @@ af: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1562,9 +1571,9 @@ af: label_attachment_plural: "Lêers" label_attribute: "Kenmerk" label_attribute_plural: "Kenmerke" - label_auth_source: "Bekragtigingsmodus" - label_auth_source_new: "Nuwe waarmerkingsmodus" - label_auth_source_plural: "Waarmerkingsmodusse" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Waarmerking" label_available_project_work_package_categories: 'Available work package categories' label_available_project_work_package_types: 'Available work package types' @@ -1730,7 +1739,7 @@ af: label_hierarchy_leaf: "Hierarchy leaf" label_home: "Tuis" label_subject_or_id: "Subject or ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "in" label_in_less_than: "in minder as" @@ -2087,10 +2096,7 @@ af: label_global_role: "Global Role" label_not_changeable: "(not changeable)" label_global: "Global" - auth_source: - using_abstract_auth_source: "Can't use an abstract authentication source." - ldap_error: "LDAP-Error: %{error_message}" - ldap_auth_failed: "Could not authenticate at the LDAP-Server." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Error executing the macro %{macro_name}" macro_unavailable: "Macro %{macro_name} cannot be displayed." macros: @@ -2279,7 +2285,7 @@ af: present_access_key_value: "Your %{key_name} is: %{value}" notice_automatic_set_of_standard_type: "Set standard type automatically." notice_logged_out: "You have been logged out." - notice_wont_delete_auth_source: The authentication mode cannot be deleted as long as there are still users using it. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields. The project is invalid: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2766,12 +2772,16 @@ af: text_work_packages_destroy_confirmation: "Are you sure you want to delete the selected work package(s)?" text_work_packages_ref_in_commit_messages: "Referencing and fixing work packages in commit messages" text_journal_added: "%{label} %{value} added" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} updated" text_journal_changed_with_diff: "%{label} changed (%{link})" text_journal_deleted: "%{label} deleted (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} deleted (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} set to %{value}" text_journal_set_with_diff: "%{label} set (%{link})" diff --git a/config/locales/crowdin/ar.yml b/config/locales/crowdin/ar.yml index 94584a1430c..bc8f18f6331 100644 --- a/config/locales/crowdin/ar.yml +++ b/config/locales/crowdin/ar.yml @@ -99,15 +99,14 @@ ar: edit: "Edit help text for %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: لا يوجد حالياً أنماط تحقيق. - no_results_content_text: أنشئ نمط تحقيق جديد background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | This LDAP form requires technical knowledge of your LDAP / Active Directory setup.
    @@ -453,16 +452,16 @@ ar: attribute_help_text: attribute_name: 'الواصفة' help_text: 'Help text' - auth_source: - account: "الحساب" - attr_firstname: "سمة الاسم الأول" - attr_lastname: "سمة الاسم الأخير" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" attr_login: "Username attribute" - attr_mail: "سمة البريد الإلكتروني" - base_dn: "قاعدة DN" - host: "المضيف" + attr_mail: "Email attribute" + base_dn: "Base DN" + host: "Host" onthefly: "Automatic user creation" - port: "منفذ" + port: "Port" tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "مستودع البيانات" @@ -561,7 +560,7 @@ ar: color: "اللون" user: admin: "مدير النظام" - auth_source: "نمط المصادقة" + ldap_auth_source: "LDAP connection" current_password: "كلمة السر الحالية" force_password_change: "تنفيذ تغيير كلمة المرور في تسجيل الدخول التالي" language: "اللغة" @@ -682,7 +681,7 @@ ar: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "هو طول خاطئ (يجب أن تكون الأحرف %{count})." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -908,8 +907,8 @@ ar: description: "'تأكيد كلمة المرور' يجب أن يطابق الإدخال في حقل 'كلمة المرور الجديدة'." status: invalid_on_create: "ليس حالة صحيحة للمستخدمين الجدد." - auth_source: - error_not_found: "لم يتم العثور" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "يرجى اختيار مستخدم واحد على الأقل أو مجموعة." role_blank: "need to be assigned." @@ -1511,6 +1510,7 @@ ar: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1525,6 +1525,8 @@ ar: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Configuration guide' get_in_touch: "You have questions? Get in touch with us." @@ -1569,6 +1571,13 @@ ar: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1637,9 +1646,9 @@ ar: label_attachment_plural: "الملفّات" label_attribute: "الواصفة" label_attribute_plural: "الواصفات" - label_auth_source: "نمط المصادقة" - label_auth_source_new: "وضع مصادقة جديد" - label_auth_source_plural: "أوضاع المصادقة" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "المصادقة" label_available_project_work_package_categories: 'فئات مجموعة العمل المتاحة' label_available_project_work_package_types: 'Available work package types' @@ -1805,7 +1814,7 @@ ar: label_hierarchy_leaf: "Hierarchy leaf" label_home: "الصفحة الرئيسية" label_subject_or_id: "Subject or ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "في" label_in_less_than: "في أقل من" @@ -2158,10 +2167,7 @@ ar: label_global_role: "الدور العالمي" label_not_changeable: "(غير قابل للتغيير)" label_global: "عالمي" - auth_source: - using_abstract_auth_source: "لا يمكن استخدام مصدر مصادقة الملخص." - ldap_error: "خطأ LDAP: %{error_message}" - ldap_auth_failed: "لا يمكن مصادقة على ملقم LDAP." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "خطأ في تنفيذ %{macro_name} الماكرو" macro_unavailable: "لا يمكن عرض الماكرو %{macro_name}." macros: @@ -2354,7 +2360,7 @@ ar: present_access_key_value: "%{key_name} الخاص بك: %{value}" notice_automatic_set_of_standard_type: "تعيين النوع القياسي تلقائياً." notice_logged_out: "لقد قمت بتسجيل الخروج." - notice_wont_delete_auth_source: لا يمكن حذف وضع المصادقة طالما لا يزال هناك مستخدمين يقومون باستخدامه. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "لا يمكنك تحديث الحقول المخصصة المتوفرة للمشروع. المشروع غير صالح: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2843,12 +2849,16 @@ ar: text_work_packages_destroy_confirmation: "هل أنت متأكد من أنك تريد حذف العناصر المحددة؟" text_work_packages_ref_in_commit_messages: "تحديد المراجع وتثبيت مجموعات العمل في رسائل commit" text_journal_added: "%{value} %{label} أضيف" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} تم تحديثه" text_journal_changed_with_diff: "%{label} تغير (%{link})" text_journal_deleted: "%{label} تم حذفه (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} تم حذفه (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} تعيين إلى %{value}" text_journal_set_with_diff: "%{label} تم تعيينه إلى (%{link})" diff --git a/config/locales/crowdin/az.yml b/config/locales/crowdin/az.yml index 229d64062e4..cc4b380471e 100644 --- a/config/locales/crowdin/az.yml +++ b/config/locales/crowdin/az.yml @@ -99,15 +99,14 @@ az: edit: "Edit help text for %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: There are currently no authentication modes. - no_results_content_text: Create a new authentication mode background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | This LDAP form requires technical knowledge of your LDAP / Active Directory setup.
    @@ -449,7 +448,7 @@ az: attribute_help_text: attribute_name: 'Attribute' help_text: 'Help text' - auth_source: + ldap_auth_source: account: "Account" attr_firstname: "Firstname attribute" attr_lastname: "Lastname attribute" @@ -557,7 +556,7 @@ az: color: "Color" user: admin: "Administrator" - auth_source: "Authentication mode" + ldap_auth_source: "LDAP connection" current_password: "Current password" force_password_change: "Enforce password change on next login" language: "Language" @@ -678,7 +677,7 @@ az: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "is the wrong length (should be %{count} characters)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -900,7 +899,7 @@ az: description: "'Password confirmation' should match the input in the 'New password' field." status: invalid_on_create: "is not a valid status for new users." - auth_source: + ldap_auth_source: error_not_found: "not found" member: principal_blank: "Please choose at least one user or group." @@ -1436,6 +1435,7 @@ az: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1450,6 +1450,8 @@ az: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Configuration guide' get_in_touch: "You have questions? Get in touch with us." @@ -1494,6 +1496,13 @@ az: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1562,9 +1571,9 @@ az: label_attachment_plural: "Files" label_attribute: "Attribute" label_attribute_plural: "Attributes" - label_auth_source: "Authentication mode" - label_auth_source_new: "New authentication mode" - label_auth_source_plural: "Authentication modes" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Authentication" label_available_project_work_package_categories: 'Available work package categories' label_available_project_work_package_types: 'Available work package types' @@ -1730,7 +1739,7 @@ az: label_hierarchy_leaf: "Hierarchy leaf" label_home: "Home" label_subject_or_id: "Subject or ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "in" label_in_less_than: "in less than" @@ -2087,10 +2096,7 @@ az: label_global_role: "Global Role" label_not_changeable: "(not changeable)" label_global: "Global" - auth_source: - using_abstract_auth_source: "Can't use an abstract authentication source." - ldap_error: "LDAP-Error: %{error_message}" - ldap_auth_failed: "Could not authenticate at the LDAP-Server." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Error executing the macro %{macro_name}" macro_unavailable: "Macro %{macro_name} cannot be displayed." macros: @@ -2279,7 +2285,7 @@ az: present_access_key_value: "Your %{key_name} is: %{value}" notice_automatic_set_of_standard_type: "Set standard type automatically." notice_logged_out: "You have been logged out." - notice_wont_delete_auth_source: The authentication mode cannot be deleted as long as there are still users using it. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields. The project is invalid: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2766,12 +2772,16 @@ az: text_work_packages_destroy_confirmation: "Are you sure you want to delete the selected work package(s)?" text_work_packages_ref_in_commit_messages: "Referencing and fixing work packages in commit messages" text_journal_added: "%{label} %{value} added" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} updated" text_journal_changed_with_diff: "%{label} changed (%{link})" text_journal_deleted: "%{label} deleted (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} deleted (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} set to %{value}" text_journal_set_with_diff: "%{label} set (%{link})" diff --git a/config/locales/crowdin/be.yml b/config/locales/crowdin/be.yml index e612b1b3c00..32d632ea454 100644 --- a/config/locales/crowdin/be.yml +++ b/config/locales/crowdin/be.yml @@ -99,15 +99,14 @@ be: edit: "Edit help text for %{attribute_caption}" enterprise: description: 'Дайце дадатковую інфармацыю для атрыбутаў(укл. карыстальніцкія)' - auth_sources: - index: - no_results_content_title: There are currently no authentication modes. - no_results_content_text: Create a new authentication mode background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | This LDAP form requires technical knowledge of your LDAP / Active Directory setup.
    @@ -451,7 +450,7 @@ be: attribute_help_text: attribute_name: 'Attribute' help_text: 'Help text' - auth_source: + ldap_auth_source: account: "Account" attr_firstname: "Firstname attribute" attr_lastname: "Lastname attribute" @@ -559,7 +558,7 @@ be: color: "Color" user: admin: "Administrator" - auth_source: "Authentication mode" + ldap_auth_source: "LDAP connection" current_password: "Current password" force_password_change: "Enforce password change on next login" language: "Language" @@ -680,7 +679,7 @@ be: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "is the wrong length (should be %{count} characters)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -904,7 +903,7 @@ be: description: "'Password confirmation' should match the input in the 'New password' field." status: invalid_on_create: "is not a valid status for new users." - auth_source: + ldap_auth_source: error_not_found: "not found" member: principal_blank: "Please choose at least one user or group." @@ -1474,6 +1473,7 @@ be: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1488,6 +1488,8 @@ be: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Configuration guide' get_in_touch: "You have questions? Get in touch with us." @@ -1532,6 +1534,13 @@ be: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1600,9 +1609,9 @@ be: label_attachment_plural: "Files" label_attribute: "Attribute" label_attribute_plural: "Attributes" - label_auth_source: "Authentication mode" - label_auth_source_new: "New authentication mode" - label_auth_source_plural: "Authentication modes" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Authentication" label_available_project_work_package_categories: 'Available work package categories' label_available_project_work_package_types: 'Available work package types' @@ -1768,7 +1777,7 @@ be: label_hierarchy_leaf: "Hierarchy leaf" label_home: "Home" label_subject_or_id: "Subject or ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "in" label_in_less_than: "in less than" @@ -2125,10 +2134,7 @@ be: label_global_role: "Global Role" label_not_changeable: "(not changeable)" label_global: "Global" - auth_source: - using_abstract_auth_source: "Can't use an abstract authentication source." - ldap_error: "LDAP-Error: %{error_message}" - ldap_auth_failed: "Could not authenticate at the LDAP-Server." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Error executing the macro %{macro_name}" macro_unavailable: "Macro %{macro_name} cannot be displayed." macros: @@ -2319,7 +2325,7 @@ be: present_access_key_value: "Your %{key_name} is: %{value}" notice_automatic_set_of_standard_type: "Set standard type automatically." notice_logged_out: "You have been logged out." - notice_wont_delete_auth_source: The authentication mode cannot be deleted as long as there are still users using it. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields. The project is invalid: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2808,12 +2814,16 @@ be: text_work_packages_destroy_confirmation: "Are you sure you want to delete the selected work package(s)?" text_work_packages_ref_in_commit_messages: "Referencing and fixing work packages in commit messages" text_journal_added: "%{label} %{value} added" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} updated" text_journal_changed_with_diff: "%{label} changed (%{link})" text_journal_deleted: "%{label} deleted (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} deleted (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} set to %{value}" text_journal_set_with_diff: "%{label} set (%{link})" diff --git a/config/locales/crowdin/bg.yml b/config/locales/crowdin/bg.yml index 2db0800e5cf..b9b45c9c9d9 100644 --- a/config/locales/crowdin/bg.yml +++ b/config/locales/crowdin/bg.yml @@ -99,15 +99,14 @@ bg: edit: "Edit help text for %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: В момента има няма режими на удостоверяване. - no_results_content_text: Създаване на нов режим на удостоверяване background_jobs: status: error_requeue: "Заданието има грешка, но се опитва отново Грешката беше: %{message}" cancelled_due_to: "Заданието е отменено поради грешка: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | Този LDAP формуляр изисква технически познания за настройката на LDAP / Active Directory.
    @@ -449,16 +448,16 @@ bg: attribute_help_text: attribute_name: 'Атрибут' help_text: 'Help text' - auth_source: - account: "Акаунт" - attr_firstname: "Лично име атрибут" - attr_lastname: "Фамилно име атрибут" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" attr_login: "Username attribute" - attr_mail: "Имейл атрибут" - base_dn: "Базов DN" - host: "Хост" + attr_mail: "Email attribute" + base_dn: "Base DN" + host: "Host" onthefly: "Automatic user creation" - port: "Порт" + port: "Port" tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "Хранилище" @@ -557,7 +556,7 @@ bg: color: "Цвят" user: admin: "Администратор" - auth_source: "Режим на удостоверяване" + ldap_auth_source: "LDAP connection" current_password: "Текуща парола" force_password_change: "Смяна на паролата при следващото влизане" language: "Език" @@ -678,7 +677,7 @@ bg: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "е грешна дължина (трябва да бъде %{count} знаци)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -900,7 +899,7 @@ bg: description: "\"Потвърждаване на паролата\" трябва да съответства на въведеното в поле \"нова парола\"." status: invalid_on_create: "is not a valid status for new users." - auth_source: + ldap_auth_source: error_not_found: "not found" member: principal_blank: "Моля изберете поне един потребител или група." @@ -1436,6 +1435,7 @@ bg: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1450,6 +1450,8 @@ bg: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Configuration guide' get_in_touch: "You have questions? Get in touch with us." @@ -1494,6 +1496,13 @@ bg: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1562,9 +1571,9 @@ bg: label_attachment_plural: "Файлове" label_attribute: "Атрибут" label_attribute_plural: "Атрибути" - label_auth_source: "Режим на удостоверяване" - label_auth_source_new: "Нов начин на оторизация" - label_auth_source_plural: "Начини на оторизация" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Оторизация" label_available_project_work_package_categories: 'Налични категории работни пакети' label_available_project_work_package_types: 'Available work package types' @@ -1730,7 +1739,7 @@ bg: label_hierarchy_leaf: "Йерархично листо" label_home: "Начална страница" label_subject_or_id: "Тема или №" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "в" label_in_less_than: "в по-малко от" @@ -2087,10 +2096,7 @@ bg: label_global_role: "Global Role" label_not_changeable: "(не може да се променя)" label_global: "Global" - auth_source: - using_abstract_auth_source: "Can't use an abstract authentication source." - ldap_error: "LDAP-Error: %{error_message}" - ldap_auth_failed: "Could not authenticate at the LDAP-Server." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Error executing the macro %{macro_name}" macro_unavailable: "Macro %{macro_name} cannot be displayed." macros: @@ -2279,7 +2285,7 @@ bg: present_access_key_value: "Your %{key_name} is: %{value}" notice_automatic_set_of_standard_type: "Задаване на стандартен тип автоматично." notice_logged_out: "Вие излязохте." - notice_wont_delete_auth_source: Режим на удостоверяване не може да бъде изтрит, докато все още има потребители, които го използват. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "Не можете да актуализирате наличните потребителски полета на проекта. Проектът е невалиден: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2766,12 +2772,16 @@ bg: text_work_packages_destroy_confirmation: "Are you sure you want to delete the selected work package(s)?" text_work_packages_ref_in_commit_messages: "Referencing and fixing work packages in commit messages" text_journal_added: "%{label} %{value} added" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} updated" text_journal_changed_with_diff: "%{label} changed (%{link})" text_journal_deleted: "%{label} deleted (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} deleted (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} set to %{value}" text_journal_set_with_diff: "%{label} set (%{link})" diff --git a/config/locales/crowdin/ca.yml b/config/locales/crowdin/ca.yml index ab95bd0fd44..769b313d36d 100644 --- a/config/locales/crowdin/ca.yml +++ b/config/locales/crowdin/ca.yml @@ -99,15 +99,14 @@ ca: edit: "Edita el text d'ajuda per a %{attribute_caption}" enterprise: description: 'Proveeix informació addicional pels atributs (inclosos camps personalitzats) dels paquets de treball o projectes. Els texts d''ajuda es mostren quan l''usuari fa clic al signe d''interrogació al costat dels valors de camp en projectes i paquets de treball.' - auth_sources: - index: - no_results_content_title: Actualment no hi ha cap mode d'autenticació. - no_results_content_text: Crea un nou mode d'autenticació background_jobs: status: error_requeue: "La tasca ha produït un error, però s'està reintentant. L'error era: %{message}" cancelled_due_to: "La feina s'ha cancel·lat a causa d'un error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | Aquest formulari LDAP requereix coneixement tècnic sobre la teva configuració LDAP / Directori actiu.
    @@ -445,17 +444,17 @@ ca: attribute_help_text: attribute_name: 'Atribut' help_text: 'Text d''ajuda' - auth_source: - account: "Compte" - attr_firstname: "Atribut Nom" - attr_lastname: "Atribut Cognom" - attr_login: "Atribut Nom d'usuari" - attr_mail: "Atribut de Correu electrònic" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" + attr_login: "Username attribute" + attr_mail: "Email attribute" base_dn: "Base DN" - host: "Servidor" - onthefly: "Creació d'usuari automàtica" + host: "Host" + onthefly: "Automatic user creation" port: "Port" - tls_certificate_string: "Servidor LDAP del certificat SSL" + tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "Repositori" comment: @@ -553,7 +552,7 @@ ca: color: "Color" user: admin: "Administrador" - auth_source: "Mode d'autenticació" + ldap_auth_source: "LDAP connection" current_password: "Contrassenya actual" force_password_change: "Forçar el canvi de contrasenya en el proper inici de sessió" language: "Idioma" @@ -674,10 +673,10 @@ ca: no està proveint un "Context Segur". Utilitza HTTPS o bé una adreça de retroalimentació, com un host local. wrong_length: "la longitud és incorrecta (haurien de ser %{count} caràcters)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: - invalid_certificate: "El certificat SSL proporcionat és invàlid: %{additional_message}" + invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" format: "%{message}" attachment: attributes: @@ -896,8 +895,8 @@ ca: description: "'Confirmació de la contrasenya' ha de coincidir amb la introduïda al camp 'Nova contrasenya'." status: invalid_on_create: "no és un valor vàlid per a nous usuaris." - auth_source: - error_not_found: "no trobat" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "Trieu com a mínim un usuari o grup." role_blank: "s'ha d'assignar." @@ -1432,6 +1431,7 @@ ca: changes_retracted: "S'han retractat els canvis." caused_changes: dates_changed: "Les dates han canviat" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: per canvis en el predecessor %{link} work_package_parent_changed_times: per canvis en el pare %{link} @@ -1446,6 +1446,8 @@ ca: dates: working: "%{date} és ara laboral" non_working: "%{date} és ara no laboral" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Guia de configuració' get_in_touch: "Tens preguntes? Posa't en contacte amb nosaltres." @@ -1490,6 +1492,13 @@ ca: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Envia una notificació per aquesta acció" work_packages: @@ -1558,9 +1567,9 @@ ca: label_attachment_plural: "Arxius" label_attribute: "Atribut" label_attribute_plural: "Atributs" - label_auth_source: "Mode d'autenticació" - label_auth_source_new: "Nou mode d'autenticació" - label_auth_source_plural: "Modes d'autenticació" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Autenticació" label_available_project_work_package_categories: 'Categories de paquet de treball disponibles' label_available_project_work_package_types: 'Classes de paquet de treball disponibles' @@ -1726,7 +1735,7 @@ ca: label_hierarchy_leaf: "Branca de jerarquia" label_home: "Inici" label_subject_or_id: "Subjecte o ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Avís legal" label_in: "en" label_in_less_than: "en menys de" @@ -2083,10 +2092,7 @@ ca: label_global_role: "Rol global" label_not_changeable: "(no variable)" label_global: "Global" - auth_source: - using_abstract_auth_source: "No podeu utilitzar una font d'autenticació abstracte." - ldap_error: "Error LDAP: %{error_message}" - ldap_auth_failed: "No s'ha pogut autenticar en el servidor de LDAP." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Error en l'execució de la macro %{macro_name}" macro_unavailable: "No es pot mostrar la macro %{macro_name}." macros: @@ -2270,7 +2276,7 @@ ca: present_access_key_value: "El teu %{key_name} és: %{value}" notice_automatic_set_of_standard_type: "Establir tipus estàndard automàticament." notice_logged_out: "S'ha tancat la sessió." - notice_wont_delete_auth_source: El mode d'autenticació no es pot suprimir mentre encara hi ha usuaris utilitzant-lo. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "No pots actualitzar els camps personalitzats disponibles del projecte. El projecte no és vàlid: %{errors}" notice_attachment_migration_wiki_page: > Aquesta pàgina s'ha generat automàticament durant l'actualització d'OpenProject. Compte tots els fitxers adjunts que anteriorment estaven associats amb el %{container_type} "%{container_name}". @@ -2755,12 +2761,16 @@ ca: text_work_packages_destroy_confirmation: "¿Estàs segur que vols eliminar el(s) paquet(s) de treball seleccionat(s)?" text_work_packages_ref_in_commit_messages: "Referència i arregla els paquets de treball en els missatges dels commits" text_journal_added: "S'ha afegit %{label} %{value}" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} ha canviat de %{old} %{linebreak}a %{new}" text_journal_changed_no_detail: "S'ha actualitzat %{label}" text_journal_changed_with_diff: "%{label} canviat (%{link})" text_journal_deleted: "%{label} s'ha suprimit (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} suprimit (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} s'ha establert a %{value}" text_journal_set_with_diff: "%{label} canviat (%{link})" diff --git a/config/locales/crowdin/ckb-IR.yml b/config/locales/crowdin/ckb-IR.yml index 1253c7b5e74..d80307dd0a4 100644 --- a/config/locales/crowdin/ckb-IR.yml +++ b/config/locales/crowdin/ckb-IR.yml @@ -99,15 +99,14 @@ ckb-IR: edit: "Edit help text for %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: There are currently no authentication modes. - no_results_content_text: Create a new authentication mode background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | This LDAP form requires technical knowledge of your LDAP / Active Directory setup.
    @@ -449,7 +448,7 @@ ckb-IR: attribute_help_text: attribute_name: 'Attribute' help_text: 'Help text' - auth_source: + ldap_auth_source: account: "Account" attr_firstname: "Firstname attribute" attr_lastname: "Lastname attribute" @@ -557,7 +556,7 @@ ckb-IR: color: "Color" user: admin: "Administrator" - auth_source: "Authentication mode" + ldap_auth_source: "LDAP connection" current_password: "Current password" force_password_change: "Enforce password change on next login" language: "Language" @@ -678,7 +677,7 @@ ckb-IR: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "is the wrong length (should be %{count} characters)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -900,7 +899,7 @@ ckb-IR: description: "'Password confirmation' should match the input in the 'New password' field." status: invalid_on_create: "is not a valid status for new users." - auth_source: + ldap_auth_source: error_not_found: "not found" member: principal_blank: "Please choose at least one user or group." @@ -1436,6 +1435,7 @@ ckb-IR: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1450,6 +1450,8 @@ ckb-IR: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Configuration guide' get_in_touch: "You have questions? Get in touch with us." @@ -1494,6 +1496,13 @@ ckb-IR: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1562,9 +1571,9 @@ ckb-IR: label_attachment_plural: "Files" label_attribute: "Attribute" label_attribute_plural: "Attributes" - label_auth_source: "Authentication mode" - label_auth_source_new: "New authentication mode" - label_auth_source_plural: "Authentication modes" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Authentication" label_available_project_work_package_categories: 'Available work package categories' label_available_project_work_package_types: 'Available work package types' @@ -1730,7 +1739,7 @@ ckb-IR: label_hierarchy_leaf: "Hierarchy leaf" label_home: "Home" label_subject_or_id: "Subject or ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "in" label_in_less_than: "in less than" @@ -2087,10 +2096,7 @@ ckb-IR: label_global_role: "Global Role" label_not_changeable: "(not changeable)" label_global: "Global" - auth_source: - using_abstract_auth_source: "Can't use an abstract authentication source." - ldap_error: "LDAP-Error: %{error_message}" - ldap_auth_failed: "Could not authenticate at the LDAP-Server." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Error executing the macro %{macro_name}" macro_unavailable: "Macro %{macro_name} cannot be displayed." macros: @@ -2279,7 +2285,7 @@ ckb-IR: present_access_key_value: "Your %{key_name} is: %{value}" notice_automatic_set_of_standard_type: "Set standard type automatically." notice_logged_out: "You have been logged out." - notice_wont_delete_auth_source: The authentication mode cannot be deleted as long as there are still users using it. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields. The project is invalid: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2766,12 +2772,16 @@ ckb-IR: text_work_packages_destroy_confirmation: "Are you sure you want to delete the selected work package(s)?" text_work_packages_ref_in_commit_messages: "Referencing and fixing work packages in commit messages" text_journal_added: "%{label} %{value} added" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} updated" text_journal_changed_with_diff: "%{label} changed (%{link})" text_journal_deleted: "%{label} deleted (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} deleted (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} set to %{value}" text_journal_set_with_diff: "%{label} set (%{link})" diff --git a/config/locales/crowdin/cs.seeders.yml b/config/locales/crowdin/cs.seeders.yml index 6eae0931848..6d06ef64a80 100644 --- a/config/locales/crowdin/cs.seeders.yml +++ b/config/locales/crowdin/cs.seeders.yml @@ -210,7 +210,7 @@ cs: subject: Organize open source conference children: item_0: - subject: Set date and location of conference + subject: Nastavit datum a místo konference children: item_0: subject: Poslat pozvánku řečníkům @@ -392,12 +392,12 @@ cs: item_7: subject: Set-up Staging environment item_8: - subject: Choose a content management system + subject: Vyberte systém pro správu obsahu item_9: - subject: Website navigation structure + subject: Navigační struktura webových stránek children: item_0: - subject: Set up navigation concept for website. + subject: Nastavte navigační koncept pro webové stránky. item_10: subject: Internal link structure item_11: @@ -411,7 +411,7 @@ cs: item_15: subject: Develop v2.0 item_16: - subject: Release v2.0 + subject: Vydání v2.0 wiki: | ### Sprint planning meeting diff --git a/config/locales/crowdin/cs.yml b/config/locales/crowdin/cs.yml index 5a12d854f43..410b1f55d58 100644 --- a/config/locales/crowdin/cs.yml +++ b/config/locales/crowdin/cs.yml @@ -99,15 +99,14 @@ cs: edit: "Upravit text nápovědy pro %{attribute_caption}" enterprise: description: 'Poskytněte další informace pro atributy (včetně vlastních polí) pracovních balíčků a projektů. Texty nápovědy se zobrazí, když uživatelé kliknou na symbol otazníku vedle vstupních polí v projektech a pracovních balíčcích.' - auth_sources: - index: - no_results_content_title: Nyní neexistují žádné ověřovací režimy. - no_results_content_text: Vytvořit nový režim ověřování background_jobs: status: error_requeue: "Úkol zaznamenal chybu, ale opakuje se. Chyba je: %{message}" cancelled_due_to: "Úkol byl zrušen z důvodu chyby: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Kliknutím sem se vrátíte do seznamu připojení.' technical_warning_html: | Tento LDAP formulář vyžaduje technické znalosti nastavení LDAP / Active Directory.
    @@ -340,9 +339,9 @@ cs: no_results_title_text: Momentálně zde nejsou žádné stavy pracovního balíčku. no_results_content_text: Přidat nový stav themes: - light: "Light" + light: "Světlý" light_high_contrast: "Light high contrast" - dark: "Dark" + dark: "Tmavý" dark_dimmed: "Dark dimmed" dark_high_contrast: "Dark high contrast" types: @@ -451,17 +450,17 @@ cs: attribute_help_text: attribute_name: 'Atribut' help_text: 'Text nápovědy' - auth_source: + ldap_auth_source: account: "Účet" - attr_firstname: "Křestní jméno" - attr_lastname: "Příjmení" - attr_login: "Atribut uživatelského jména" - attr_mail: "Email" + attr_firstname: "Jméno (atribut)" + attr_lastname: "Příjmení (atribut)" + attr_login: "Uživatelské jméno (atribut)" + attr_mail: "Email (atribut)" base_dn: "Base DN" - host: "Hostitel" - onthefly: "Automatické vytváření uživatele" + host: "Host" + onthefly: "Automatic user creation" port: "Port" - tls_certificate_string: "SSL certifikát LDAP serveru" + tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "Repozitář" comment: @@ -559,7 +558,7 @@ cs: color: "Barva" user: admin: "Administrátor" - auth_source: "Režim ověřování" + ldap_auth_source: "LDAP connection" current_password: "Aktuální přístupové heslo" force_password_change: "Vynutit změnu hesla při příštím přihlášení" language: "Jazyk" @@ -680,7 +679,7 @@ cs: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "má chybnou délku (měla by být %{count} znaků)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -904,7 +903,7 @@ cs: description: "\"Potvrzení hesla\" musí odpovídat vstupu v poli \"Nové heslo\"." status: invalid_on_create: "není platný stav pro nové uživatele." - auth_source: + ldap_auth_source: error_not_found: "nenalezeno" member: principal_blank: "Zvolte alespoň jednoho uživatele nebo skupinu." @@ -1474,6 +1473,7 @@ cs: changes_retracted: "Změny byly staženy." caused_changes: dates_changed: "Data změněna" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1488,6 +1488,8 @@ cs: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Konfigurační manuál' get_in_touch: "Máte otázky? Spojte se s námi." @@ -1497,7 +1499,7 @@ cs: menus: admin: mail_notification: "E-mailová upozornění" - mails_and_notifications: "Emails and notifications" + mails_and_notifications: "E-maily a oznámení" aggregation: 'Agregace' api_and_webhooks: "API & Webhooky" quick_add: @@ -1516,7 +1518,7 @@ cs: api: title: "API" text_hint: "API tokens allow third-party applications to communicate with this OpenProject instance via REST APIs." - static_token_name: "API token" + static_token_name: "API Token" disabled_text: "API tokens are not enabled by the administrator. Please contact your administrator to use this feature." ical: title: "iCalendar" @@ -1532,6 +1534,13 @@ cs: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Odeslat oznámení pro tuto akci" work_packages: @@ -1600,9 +1609,9 @@ cs: label_attachment_plural: "Soubory" label_attribute: "Atribut" label_attribute_plural: "Atributy" - label_auth_source: "Režim ověřování" - label_auth_source_new: "Nový režim ověřování" - label_auth_source_plural: "Režimy ověřování" + label_ldap_auth_source_new: "Nové připojení LDAP" + label_ldap_auth_source: "Připojení LDAP" + label_ldap_auth_source_plural: "Připojení LDAP" label_authentication: "Ověření" label_available_project_work_package_categories: 'Dostupné kategorie pracovních balíčků' label_available_project_work_package_types: 'Dostupné kategorie pracovních balíčků' @@ -1723,7 +1732,7 @@ cs: label_enterprise: "Podniky" label_enterprise_active_users: "%{current}/%{limit} rezervováno aktivních uživatelů" label_enterprise_edition: "Enterprise Edice" - label_enterprise_support: "Enterprise support" + label_enterprise_support: "Podpora pro Enterprise edici" label_environment: "Prostředí" label_estimates_and_time: "Odhady a čas" label_equals: "je" @@ -1736,7 +1745,7 @@ cs: label_expanded_click_to_collapse: "Rozbaleno. Klepnutím sbalte" label_f_hour: "%{value} hodina" label_f_hour_plural: "%{value} hodin" - label_favoured: "Favoured" + label_favoured: "Oblíbené" label_feed_plural: "Informační kanály (feedy)" label_feeds_access_key: "Přístupový klíč pro RSS" label_feeds_access_key_created_on: "Přístupový klíč pro RSS byl vytvořen před %{value}" @@ -1768,7 +1777,7 @@ cs: label_hierarchy_leaf: "Úroveň hierarchie" label_home: "Domů" label_subject_or_id: "Předmět nebo ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Právní oznámení" label_in: "v" label_in_less_than: "za méně než" @@ -2125,10 +2134,7 @@ cs: label_global_role: "Globální role" label_not_changeable: "(neměnitelné)" label_global: "Globální" - auth_source: - using_abstract_auth_source: "Nelze použít abstraktní zdroj ověřování." - ldap_error: "Chyba LDAP: %{error_message}" - ldap_auth_failed: "Nelze autentizovat u serveru LDAP." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Chyba při provádění makra %{macro_name}" macro_unavailable: "Makro %{macro_name} nelze zobrazit." macros: @@ -2318,7 +2324,7 @@ cs: present_access_key_value: "Váš %{key_name} je: %{value}" notice_automatic_set_of_standard_type: "Automaticky nastavit standardní typ." notice_logged_out: "Byli jste odhlášeni." - notice_wont_delete_auth_source: Režim ověřování nemůže být odstraněn, dokud ho stále používají uživatelé. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "Nemůžete aktualizovat dostupné vlastní pole projektu. Projekt je neplatný: %{errors}" notice_attachment_migration_wiki_page: > Tato stránka byla generována automaticky během aktualizace OpenProject. Obsahuje všechny přílohy dříve přidružené k %{container_type} "%{container_name}". @@ -2807,12 +2813,16 @@ cs: text_work_packages_destroy_confirmation: "Jste si jisti, že chcete odstranit vybrané pracovní balíčky?" text_work_packages_ref_in_commit_messages: "Odkazování a opravování pracovních balíčků ve zprávách commitu" text_journal_added: "%{label} %{value} přidán" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} se změnil z %{old} %{linebreak}na %{new}" text_journal_changed_no_detail: "%{label} aktualizován" text_journal_changed_with_diff: "%{label} změněn (%{link})" text_journal_deleted: "%{label} smazán (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} smazán (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} nastaven na %{value}" text_journal_set_with_diff: "%{label} nastaven (%{link})" @@ -2862,7 +2872,7 @@ cs: views: project: "%{plural} are always attached to a project. After creating a %{singular} you can add work packages from other projects to it." public: "Publish this view, allowing other users to access your view. Users with the 'Manage public views' permission can modify or remove public query. This does not affect the visibility of work package results in that view and depending on their permissions, users may see different results." - favoured: "Mark this view as favourite and add to the saved views sidebar on the left." + favoured: "Označte toto zobrazení jako oblíbené a přidejte do postranního panelu vlevo." time: am: "am" formats: @@ -2972,7 +2982,7 @@ cs: warning: > Changing which days of the week are considered working days or non-working days can affect the start and finish days of all work packages in all projects in this instance.
    Please note that changes are only applied after you click on the apply changes button. journal_note: - changed: _**Working days** changed (%{changes})._ + changed: _**Pracovní dny** změněny (%{changes})._ days: working: "%{day} je pracovní " non_working: "%{day} je nepracovní " diff --git a/config/locales/crowdin/da.yml b/config/locales/crowdin/da.yml index 7e4e2d349fc..39b84656023 100644 --- a/config/locales/crowdin/da.yml +++ b/config/locales/crowdin/da.yml @@ -99,15 +99,14 @@ da: edit: "Redigér hjælpetekst til %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: Der er i øjeblikket ingen godkendelse. - no_results_content_text: Opret en ny godkendelse background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | Denne LDAP-formular kræver teknisk viden om din LDAP/Active Directory opsætning.
    @@ -447,14 +446,14 @@ da: attribute_help_text: attribute_name: 'Egenskab' help_text: 'Help text' - auth_source: - account: "Konto" - attr_firstname: "Fornavn" - attr_lastname: "Efternavn" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" attr_login: "Username attribute" - attr_mail: "E-mail" - base_dn: "DN base" - host: "Vært" + attr_mail: "Email attribute" + base_dn: "Base DN" + host: "Host" onthefly: "Automatic user creation" port: "Port" tls_certificate_string: "LDAP server SSL certificate" @@ -555,7 +554,7 @@ da: color: "Farve" user: admin: "Administrator" - auth_source: "Godkendelsestilstand" + ldap_auth_source: "LDAP connection" current_password: "Nuværende adgangskode" force_password_change: "Ændring af adgangskode virksom ved næste login" language: "Sprog" @@ -676,7 +675,7 @@ da: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "har forkert længde (burde være %{count} tegn)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -898,7 +897,7 @@ da: description: "'Adgangskodebekræftelse' skal matche input fra feltet 'Ny adgangskode'." status: invalid_on_create: "is not a valid status for new users." - auth_source: + ldap_auth_source: error_not_found: "not found" member: principal_blank: "Vælg venligst mindst én bruger eller gruppe." @@ -1434,6 +1433,7 @@ da: changes_retracted: "Ændringerne blev trukket tilbage." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1448,6 +1448,8 @@ da: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Configuration guide' get_in_touch: "Har du spørgsmål? Kom i kontakt med os." @@ -1492,6 +1494,13 @@ da: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1560,9 +1569,9 @@ da: label_attachment_plural: "Filer" label_attribute: "Egenskab" label_attribute_plural: "Egenskaber" - label_auth_source: "Godkendelsestilstand" - label_auth_source_new: "Ny godkendelsestilstand" - label_auth_source_plural: "Godkendelsestilstande" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Godkendelse" label_available_project_work_package_categories: 'Tilgængelige arbejdspakkekategorier' label_available_project_work_package_types: 'Available work package types' @@ -1728,7 +1737,7 @@ da: label_hierarchy_leaf: "Hierarchy leaf" label_home: "Hjem" label_subject_or_id: "Subject or ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "i" label_in_less_than: "på mindre end" @@ -2085,10 +2094,7 @@ da: label_global_role: "Global Role" label_not_changeable: "(not changeable)" label_global: "Global" - auth_source: - using_abstract_auth_source: "Kan ikke anvende en abstrakt godkendelseskilde." - ldap_error: "LDAP-fejl: %{error_message}" - ldap_auth_failed: "Kunne ikke godkendes på LDAP-serveren." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Fejl ved udførelse af makroen %{macro_name}" macro_unavailable: "Makro %{macro_name} kan ikke vises." macros: @@ -2277,7 +2283,7 @@ da: present_access_key_value: "Din %{key_name} er: %{value}" notice_automatic_set_of_standard_type: "Fastsæt typen automatisk." notice_logged_out: "Du er blevet logget ud." - notice_wont_delete_auth_source: Godkendelsestilstanden kan ikke slettes, så længe den er stadig benyttes af brugere. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields. The project is invalid: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2762,12 +2768,16 @@ da: text_work_packages_destroy_confirmation: "Er du sikker på, du vil slette de/den valgte arbejdspakke(r)?" text_work_packages_ref_in_commit_messages: "Arbejdspakker håndteres i tilknyttede meddelelser" text_journal_added: "%{label} %{value} tilføjet" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} opdateret" text_journal_changed_with_diff: "%{label} ændret (%{link})" text_journal_deleted: "%{label} slettet (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} slettet (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} sat til %{value}" text_journal_set_with_diff: "%{label} sat (%{link})" diff --git a/config/locales/crowdin/de.yml b/config/locales/crowdin/de.yml index 773b9485722..a1e3426e0c9 100644 --- a/config/locales/crowdin/de.yml +++ b/config/locales/crowdin/de.yml @@ -99,15 +99,14 @@ de: edit: "Hilfe-Text für %{attribute_caption} bearbeiten" enterprise: description: 'Geben Sie zusätzliche Informationen für Attribute (inkl. benutzerdefinierter Felder) von Arbeitspaketen und Projekten an. Hilfetexte werden angezeigt, wenn Benutzer in Projekten und Arbeitspaketen auf das Fragezeichensymbol neben Eingabefeldern klicken.' - auth_sources: - index: - no_results_content_title: Derzeit gibt es keine Authentifizierungsmodi. - no_results_content_text: Erstellen Sie einen neuen Authentifizierungsmodus background_jobs: status: error_requeue: "Der Job ist mit einem Fehler fehlgeschlagen, wird aber erneut eingestellt. Der Fehler war: %{message}" cancelled_due_to: "Job wurde aufgrund eines Fehlers abgebrochen: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | Dieses LDAP-Formular erfordert technische Kenntnisse ihres LDAP-/Active Directory Setups.
    @@ -166,18 +165,18 @@ de: index: no_results_title_text: Derzeit gibt es keine Farben. no_results_content_text: Neue Farbe anlegen - label_new_color: "New color" + label_new_color: "Neue Farbe" new: - label_new_color: "New Color" + label_new_color: "Neue Farbe" edit: - label_edit_color: "Edit Color" + label_edit_color: "Farbe bearbeiten" form: - label_new_color: "New color" - label_edit_color: "Edit color" + label_new_color: "Neue Farbe" + label_edit_color: "Farbe bearbeiten" label_no_color: "Keine Farbe" - label_properties: "Properties" + label_properties: "Eigenschaften" label_really_delete_color: > - Are you sure, you want to delete the following color? Types using this color will not be deleted. + Möchten Sie die folgende Farbe wirklich löschen? Typen, die diese Farbe verwenden, bleiben erhalten. custom_actions: actions: name: "Aktionen" @@ -260,7 +259,7 @@ de: types: no_results_title_text: Derzeit stehen keine Typen zur Verfügung. form: - enable_type_in_project: 'Enable type "%{type}"' + enable_type_in_project: 'Typ "%{type}" aktivieren' versions: no_results_title_text: Derzeit gibt es keine Versionen für das Projekt. no_results_content_text: Neue Version erstellen @@ -277,9 +276,9 @@ de: notice_reset_token: "Ein neuer %{type} Token wurde generiert. Der Zutrittstoken lautet:" token_value_warning: "Hinweis: Dieser Token ist nur einmal sichtbar, der Token kann jetzt kopiert werden." no_results_title_text: Zur Zeit stehen keine Zugangs-Tokens zur Verfügung. - notice_api_token_revoked: "The API token has been deleted. To create a new token please use the link in the API section." - notice_rss_token_revoked: "The RSS token has been deleted. To create a new token please use the link in the RSS section." - notice_ical_token_revoked: "iCalendar token \"%{token_name}\" for calendar \"%{calendar_name}\" of project \"%{project_name}\" has been revoked. The iCalendar URL with this token is now invalid." + notice_api_token_revoked: "Der API-Token wurde gelöscht. Um ein neues Token zu erstellen, benutzen Sie bitte den Link im API-Bereich." + notice_rss_token_revoked: "Der RSS-Token wurde gelöscht. Um ein neues Token zu erstellen, benutzen Sie bitte den Link im RSS-Bereich." + notice_ical_token_revoked: "iCalendar-Token „%{token_name}“ für Kalender „%{calendar_name}“ des Projekts „%{project_name}“ wurde widerrufen. Die iCal-URL mit diesem Token ist jetzt ungültig." news: index: no_results_title_text: Es gibt aktuell keine Neuigkeiten. @@ -335,11 +334,11 @@ de: no_results_title_text: Derzeit gibt es keine Arbeitspaket-Status. no_results_content_text: Neuen Status hinzufügen themes: - light: "Light" - light_high_contrast: "Light high contrast" - dark: "Dark" - dark_dimmed: "Dark dimmed" - dark_high_contrast: "Dark high contrast" + light: "Hell" + light_high_contrast: "Hell (hoher Kontrast)" + dark: "Dunkel" + dark_dimmed: "Dunkel gedimmt" + dark_high_contrast: "Dunkel (hoher Kontrast)" types: index: no_results_title_text: Derzeit gibt es keine Typen. @@ -444,17 +443,17 @@ de: attribute_help_text: attribute_name: 'Attribut' help_text: 'Hilfe-Text' - auth_source: - account: "Konto" - attr_firstname: "Vorname-Attribut" - attr_lastname: "Nachname-Attribut" - attr_login: "Benutzername" - attr_mail: "E-Mail-Attribut" - base_dn: "BASE-DN" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" + attr_login: "Username attribute" + attr_mail: "Email attribute" + base_dn: "Base DN" host: "Host" - onthefly: "Benutzer automatisch erzeugen" + onthefly: "Automatic user creation" port: "Port" - tls_certificate_string: "LDAP-Server SSL-Zertifikat" + tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "Projektarchiv" comment: @@ -552,7 +551,7 @@ de: color: "Farbe" user: admin: "Administrator" - auth_source: "Authentifizierungs-Modus" + ldap_auth_source: "LDAP connection" current_password: "Aktuelles Passwort" force_password_change: "Erzwinge Passwortwechsel beim nächsten Login" language: "Sprache" @@ -673,10 +672,10 @@ de: stellt keinen "Secure Context" zur Verfügung: Benutzen Sie entweder HTTPS oder eine Loopback-Adresse, wie z.B. localhost. wrong_length: "hat die falsche Länge (muss genau %{count} Zeichen haben)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: - invalid_certificate: "Das angegebene SSL-Zertifikat ist ungültig: %{additional_message}" + invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" format: "%{message}" attachment: attributes: @@ -895,8 +894,8 @@ de: description: "\"Kennwortbestätigung\" sollte mit \"Neues Kennwort\" übereinstimmen." status: invalid_on_create: "ist kein gültiger Status für neue Benutzer." - auth_source: - error_not_found: "nicht gefunden" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "Bitte wählen Sie mindestens einen Benutzer oder eine Gruppe." role_blank: "muss zugeordnet werden." @@ -1314,7 +1313,7 @@ de: error_can_not_unarchive_project: "Dieses Projekt kann nicht ent-archiviert werden: %{errors}" error_check_user_and_role: "Bitte wählen Sie einen Nutzer und eine Rolle." error_code: "Fehler %{code}" - error_color_could_not_be_saved: "Color could not be saved" + error_color_could_not_be_saved: "Farbe konnte nicht gespeichert werden" error_cookie_missing: 'Das OpenProject Cookie fehlt. Bitte stellen Sie sicher, dass Cookies aktiviert sind, da diese Applikation ohne aktivierte Cookies nicht korrekt funktioniert.' error_custom_option_not_found: "Option ist nicht vorhanden." error_enterprise_activation_user_limit: "Ihr Konto konnte nicht aktiviert werden (Nutzerlimit erreicht). Bitte kontaktieren Sie Ihren Administrator um Zugriff zu erhalten." @@ -1339,7 +1338,7 @@ de: error_password_change_failed: 'Beim Ändern des Passworts ist ein Fehler aufgetreten.' error_scm_command_failed: "Beim Zugriff auf das Projektarchiv ist ein Fehler aufgetreten: %{value}" error_scm_not_found: "Eintrag und/oder Revision existiert nicht im Projektarchiv." - error_type_could_not_be_saved: "Type could not be saved" + error_type_could_not_be_saved: "Typ konnte nicht gespeichert werden." error_unable_delete_status: "Der Arbeitspaket-Status kann nicht gelöscht werden, da er von mindestens einem Arbeitspaket genutzt wird." error_unable_delete_default_status: "Der Standardstatus für neue Arbeitspakete kann nicht gelöscht werden. Bitte wählen Sie einen anderen Standardstatus, bevor Sie diesen Status löschen." error_unable_to_connect: "Fehler beim Verbinden (%{value})" @@ -1431,6 +1430,7 @@ de: changes_retracted: "Die Änderungen wurden zurückgezogen." caused_changes: dates_changed: "Datum geändert" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: durch Änderungen am Vorgänger %{link} work_package_parent_changed_times: durch Änderungen am übergeordneten Arbeitspaket %{link} @@ -1445,6 +1445,8 @@ de: dates: working: "%{date} ist jetzt ein Arbeitstag" non_working: "%{date} ist jetzt ein arbeitsfreier Tag" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Konfigurationsanleitung' get_in_touch: "Sie haben Fragen? Nehmen Sie Kontakt mit uns auf." @@ -1454,7 +1456,7 @@ de: menus: admin: mail_notification: "Mailbenachrichtigung" - mails_and_notifications: "Emails and notifications" + mails_and_notifications: "E-Mails und Benachrichtigungen" aggregation: 'Zusammenfassungen' api_and_webhooks: "API und Webhooks" quick_add: @@ -1469,26 +1471,33 @@ de: action: "Aktion" expiration: "Läuft aus" indefinite_expiration: "Nie" - simple_revoke_confirmation: "Are you sure you want to revoke this token?" + simple_revoke_confirmation: "Sind Sie sicher, dass Sie dieses Token widerrufen möchten?" api: - title: "API" - text_hint: "API tokens allow third-party applications to communicate with this OpenProject instance via REST APIs." - static_token_name: "API token" - disabled_text: "API tokens are not enabled by the administrator. Please contact your administrator to use this feature." + title: "Schnittstelle (API)" + text_hint: "API-Token erlauben es Drittanbieter-Anwendungen, mit dieser OpenProject-Instanz über REST-APIs zu kommunizieren." + static_token_name: "API-Token" + disabled_text: "API-Token sind vom Administrator nicht aktiviert. Bitte kontaktieren Sie Ihren Administrator, um diese Funktion zu nutzen." ical: title: "iCalendar" - text_hint: "iCalendar tokens allow users to subscribe to OpenProject calendars and view up-to-date work package information from external clients." - disabled_text: "iCalendar subscriptions are not enabled by the administrator. Please contact your administrator to use this feature." - empty_text_hint: "To add an iCalendar token, subscribe to a new or existing calendar from within the Calendar module of a project. You must have the necessary permissions." + text_hint: "iCalendar Token erlauben es Benutzern OpenProject Kalender zu abonnieren und aktuelle Arbeitspaket-Informationen auf externen Clients anzuzeigen." + disabled_text: "iCalendar Abonnements sind vom Administrator nicht aktiviert. Bitte kontaktieren Sie Ihren Administrator, um diese Funktion zu nutzen." + empty_text_hint: "Um einen iCalendar Token hinzuzufügen, abonnieren Sie einen neuen oder bestehenden Kalender innerhalb des Kalender-Moduls eines Projekts. Sie müssen über die erforderlichen Berechtigungen verfügen." oauth: title: "OAuth" - text_hint: "OAuth tokens allow third-party applications to connect with this OpenProject instance." - empty_text_hint: "There is no third-party application access configured and active for you. Please contact your administrator to activate this feature." + text_hint: "OAuth Tokens erlauben es Drittanbieter-Anwendungen, sich mit dieser OpenProject Instanz zu verbinden." + empty_text_hint: "Für Sie ist kein Zugriff auf die Anwendung von Drittanbietern konfiguriert. Bitte kontaktieren Sie Ihren Administrator, um diese Funktion zu aktivieren." rss: title: "RSS" - text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." - static_token_name: "RSS token" - disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + text_hint: "RSS-Token erlauben es Benutzern über einen externen RSS-Reader auf dem Laufenden zu bleiben." + static_token_name: "RSS-Token" + disabled_text: "RSS-Token sind vom Administrator nicht aktiviert. Bitte kontaktieren Sie Ihren Administrator, um diese Funktion zu nutzen." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Sende Benachrichtigungen für diese Aktion" work_packages: @@ -1557,9 +1566,9 @@ de: label_attachment_plural: "Dateien" label_attribute: "Attribut" label_attribute_plural: "Attribute" - label_auth_source: "Authentifizierungs-Modus" - label_auth_source_new: "Neuer Authentifizierungs-Modus" - label_auth_source_plural: "Authentifizierungs-Arten" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Authentifizierung" label_available_project_work_package_categories: 'Verfügbare Arbeitspaket-Kategorien' label_available_project_work_package_types: 'Verfügbare Arbeitspaket-Typen' @@ -1618,7 +1627,7 @@ de: label_confirmation: "Bestätigung" label_contains: "enthält" label_content: "Inhalt" - label_color_plural: "Colors" + label_color_plural: "Farben" label_copied: "kopiert" label_copy_same_as_target: "So wie das Ziel" label_copy_source: "Quelle" @@ -1693,7 +1702,7 @@ de: label_expanded_click_to_collapse: "Erweitert. Klicken Sie zum Ausblenden" label_f_hour: "%{value} Stunde" label_f_hour_plural: "%{value} Stunden" - label_favoured: "Favoured" + label_favoured: "Favorisiert" label_feed_plural: "Feeds" label_feeds_access_key: "RSS-Zugriffsschlüssel" label_feeds_access_key_created_on: "Atom-Zugriffsschlüssel vor %{value} erstellt" @@ -1725,7 +1734,7 @@ de: label_hierarchy_leaf: "Hierarchie-Blatt" label_home: "Hauptseite" label_subject_or_id: "Titel oder ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Kalenderabonnements" label_impressum: "Impressum" label_in: "an" label_in_less_than: "in weniger als" @@ -1889,8 +1898,8 @@ de: label_project_new: "Neues Projekt" label_project_plural: "Projekte" label_project_settings: "Projektkonfiguration" - label_project_storage_plural: "File Storages" - label_project_storage_project_folder: "File Storages: Project folders" + label_project_storage_plural: "Dateispeicher" + label_project_storage_project_folder: "Dateispeicher: Projektordner" label_projects_storage_information: "%{count} Projekte verbrauchen insgesamt %{storage} Speicherplatz" label_project_view_all: "Alle Projekte anzeigen" label_project_show_details: "Projektdetails anzeigen" @@ -1978,7 +1987,7 @@ de: label_this_week: "aktuelle Woche" label_this_year: "aktuelles Jahr" label_time_entry_plural: "Aufgewendete Zeit" - label_title: "Title" + label_title: "Titel" label_projects_menu: "Projekte" label_today: "heute" label_top_menu: "Hauptmenü" @@ -2082,10 +2091,7 @@ de: label_global_role: "Globale Rolle" label_not_changeable: "(nicht veränderbar)" label_global: "Global" - auth_source: - using_abstract_auth_source: "Abstrakte Authentifizierungsquellen können nicht verwendet werden." - ldap_error: "LDAP-Fehler: %{error_message}" - ldap_auth_failed: "Auf dem LDAP-Server konnte nicht authentifiziert werden." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Fehler beim Ausführen des Makros %{macro_name}" macro_unavailable: "Das Makro %{macro_name} kann nicht angezeigt werden." macros: @@ -2274,7 +2280,7 @@ de: present_access_key_value: "Ihr %{key_name} ist: %{value}" notice_automatic_set_of_standard_type: "Der Standard-Typ wurde automatisch gesetzt." notice_logged_out: "Sie wurden ausgeloggt." - notice_wont_delete_auth_source: Die Authentifizierungsmethode kann nicht gelöscht werden, solange es noch Benutzer gibt, die diese verwenden. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "Sie können die benutzerdefinierten Felder dieses Projekts nicht aktualisieren. Das Projekt ist ungültig: %{errors}" notice_attachment_migration_wiki_page: > Diese Seite wurde automatisch während der Aktualisierung von OpenProject generiert. Es enthält alle Anhänge, die zuvor innerhalb %{container_type} "%{container_name}" zugeordnet waren. @@ -2308,8 +2314,8 @@ de: permission_add_messages: "Forenbeiträge hinzufügen" permission_add_project: "Projekt erstellen" permission_archive_project: "Projekt archivieren" - permission_create_user: "Create users" - permission_manage_user: "Edit users" + permission_create_user: "Benutzer erstellen" + permission_manage_user: "Benutzer bearbeiten" permission_manage_placeholder_user: "Platzhalter Benutzer erstellen, bearbeiten und löschen" permission_add_subprojects: "Unterprojekte erstellen" permission_add_work_package_watchers: "Beobachter hinzufügen" @@ -2761,12 +2767,16 @@ de: text_work_packages_destroy_confirmation: "Sind Sie sicher, dass Sie die ausgewählten Arbeitspakete löschen möchten?" text_work_packages_ref_in_commit_messages: "Arbeitspaket-Beziehungen und -Status in Commit-Log-Meldungen" text_journal_added: "%{label} %{value} wurde hinzugefügt" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} geändert von %{old} %{linebreak}zu %{new}" text_journal_changed_no_detail: "%{label} aktualisiert" text_journal_changed_with_diff: "%{label} geändert (%{link})" text_journal_deleted: "%{label} %{old} wurde gelöscht" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} wurde gelöscht (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} wurde auf %{value} gesetzt" text_journal_set_with_diff: "%{label} gesetzt (%{link})" @@ -2814,9 +2824,9 @@ de: text_setup_mail_configuration: "Konfigurieren Sie Ihren E-Mail-Provider" help_texts: views: - project: "%{plural} are always attached to a project. After creating a %{singular} you can add work packages from other projects to it." - public: "Publish this view, allowing other users to access your view. Users with the 'Manage public views' permission can modify or remove public query. This does not affect the visibility of work package results in that view and depending on their permissions, users may see different results." - favoured: "Mark this view as favourite and add to the saved views sidebar on the left." + project: "%{plural} sind immer einem Projekt zugeordnet. Nach dem Erstellen eines %{singular} können Sie Arbeitspakete von anderen Projekten hinzufügen." + public: "Veröffentlichen Sie diese Ansicht und erlauben Sie anderen Benutzern, auf sie zuzugreifen. Benutzer mit der Berechtigung \"Öffentliche Ansichten verwalten\" können öffentliche Abfragen ändern oder entfernen. Dies hat keinen Einfluss auf die Sichtbarkeit von Arbeitspaketen in dieser Ansicht und je nach Berechtigung, können Benutzer unterschiedliche Ergebnisse sehen." + favoured: "Markiert diese Ansicht als Favorit und fügt sie der linken Seitenleiste hinzu." time: am: "vormittags" formats: diff --git a/config/locales/crowdin/el.yml b/config/locales/crowdin/el.yml index 895405028b2..2b5144658f5 100644 --- a/config/locales/crowdin/el.yml +++ b/config/locales/crowdin/el.yml @@ -99,15 +99,14 @@ el: edit: "Επεξεργασία βοηθητικού κειμένου για %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: Αυτήν τη στιγμή δεν υπάρχουν λειτουργίες ταυτοποίησης. - no_results_content_text: Δημιουργήστε μια νέα λειτουργία ταυτοποίησης background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Η εργασία ακυρώθηκε λόγω σφάλματος: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | Αυτή η φόρμα LDAP απαιτεί τεχνικές γνώσεις για την LDAP / Active Directory εγκατάσταση σας.
    @@ -445,16 +444,16 @@ el: attribute_help_text: attribute_name: 'Χαρακτηριστικό' help_text: 'Κείμενο βοήθειας' - auth_source: - account: "Λογαριασμός" - attr_firstname: "Χαρακτηριστικό ονόματος" - attr_lastname: "Χαρακτηριστικό επωνύμου" - attr_login: "Χαρακτηριστικό ονόματος χρήστη" - attr_mail: "Χαρακτηριστικό email" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" + attr_login: "Username attribute" + attr_mail: "Email attribute" base_dn: "Base DN" - host: "Διακομιστής" - onthefly: "Αυτόματη δημιουργία χρήστη" - port: "Θύρα" + host: "Host" + onthefly: "Automatic user creation" + port: "Port" tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "Αποθετήριο" @@ -553,7 +552,7 @@ el: color: "Χρώμα" user: admin: "Διαχειριστής" - auth_source: "Τρόπος αυθεντικοποίησης" + ldap_auth_source: "LDAP connection" current_password: "Τρέχων κωδικός πρόσβασης" force_password_change: "Επιβολή αλλαγής κωδικού πρόσβασης στην επόμενη σύνδεση" language: "Γλώσσα" @@ -674,7 +673,7 @@ el: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "έχει λάθος μέγεθος (πρέπει να είναι %{count} χαρακτήρες)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -896,8 +895,8 @@ el: description: "Ο 'Κωδικός Επιβεβαίωσης' θα πρέπει να ταιριάζει με την είσοδο του πεδίου 'Νέος Κωδικός'." status: invalid_on_create: "δεν είναι έγκυρη κατάσταση για νέους χρήστες." - auth_source: - error_not_found: "δεν βρέθηκε" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "Παρακαλούμε επιλέξτε τουλάχιστον ένα χρήστη ή ομάδα." role_blank: "πρέπει να ανατεθεί." @@ -1432,6 +1431,7 @@ el: changes_retracted: "Οι αλλαγές αποσύρθηκαν." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1446,6 +1446,8 @@ el: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Οδηγός διαμόρφωσης' get_in_touch: "Έχετε απορίες; Επικοινωνήστε μαζί μας." @@ -1490,6 +1492,13 @@ el: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1558,9 +1567,9 @@ el: label_attachment_plural: "Αρχεία" label_attribute: "Χαρακτηριστικό" label_attribute_plural: "Χαρακτηριστικά" - label_auth_source: "Τρόπος αυθεντικοποίησης" - label_auth_source_new: "Νέος τρόπος ταυτοποίησης" - label_auth_source_plural: "Τρόποι ταυτοποίησης" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Ταυτοποίηση" label_available_project_work_package_categories: 'Διαθέσιμες κατηγορίες πακέτων εργασίας' label_available_project_work_package_types: 'Διαθέσιμοι τύποι εργασιών' @@ -1726,7 +1735,7 @@ el: label_hierarchy_leaf: "Φύλλο ιεραρχίας" label_home: "Αρχική" label_subject_or_id: "Θέμα ή ταυτότητα" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Νομική Ειδοποίηση" label_in: "σε" label_in_less_than: "σε λιγότερο από" @@ -2083,10 +2092,7 @@ el: label_global_role: "Καθολικός Ρόλος" label_not_changeable: "(μη μεταβλητό)" label_global: "Καθολικό" - auth_source: - using_abstract_auth_source: "Δεν μπορεί να χρησιμοποιηθεί μια αφηρημένη πηγή αυθεντικοποίησης." - ldap_error: "Σφάλμα-LDAP: %{error_message}" - ldap_auth_failed: "Δεν ήταν δυνατή η ταυτοποίηση στον LDAP-Server." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Σφάλμα στην εκτέλεση της μακροεντολής %{macro_name}" macro_unavailable: "Η μακροεντολή %{macro_name} δεν μπορεί να εμφανιστεί." macros: @@ -2274,7 +2280,7 @@ el: present_access_key_value: "Το %{key_name} είναι: %{value}" notice_automatic_set_of_standard_type: "Ορισμός κανονικού τύπου αυτόματα." notice_logged_out: "Έχετε αποσυνδεθεί." - notice_wont_delete_auth_source: Η τρόπος ταυτοποίησης δεν μπορεί να διαγραφεί όσο υπάρχουν ακόμα χρήστες που το χρησιμοποιούν. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "Δεν μπορείτε να ενημερώσετε τα διαθέσιμα προσαρμοσμένα πεδία του έργου. Το έργο δεν είναι έγκυρο: %{errors}" notice_attachment_migration_wiki_page: > Αυτή η σελίδα δημιουργήθηκε αυτόματα κατά την διάρκεια της ενημέρωσης του OpenProject. Περιέχει όλα τα συνημμένα που σχετίζονταν προηγουμένως με το %{container_type} "%{container_name}". @@ -2761,12 +2767,16 @@ el: text_work_packages_destroy_confirmation: "Είστε βέβαιοι ότι θέλετε να διαγράψετε το(-α) επιλεγμένο(-α) πακέτο(-α) εργασίας;" text_work_packages_ref_in_commit_messages: "Αναφορά και επιδιόρθωση πακέτων εργασίας σε μηνύματα δέσμευσης" text_journal_added: "%{label} %{value} προστέθηκε" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "Το %{label} ενημερώθηκε" text_journal_changed_with_diff: "Το %{label} άλλαξε (%{link})" text_journal_deleted: "Το %{label} διαγράφηκε (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "Το %{label} διαγράφηκε (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "Το %{label} ορίστηκε σε %{value}" text_journal_set_with_diff: "Το %{label} ορίστηκε (%{link})" diff --git a/config/locales/crowdin/eo.yml b/config/locales/crowdin/eo.yml index a6de34e71c7..b9198aa2b9d 100644 --- a/config/locales/crowdin/eo.yml +++ b/config/locales/crowdin/eo.yml @@ -99,15 +99,14 @@ eo: edit: "Redakti helptekston por %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: Aktuale estas neniu aŭtentiga reĝimo. - no_results_content_text: Krei novan aŭtentigan reĝimon. background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | This LDAP form requires technical knowledge of your LDAP / Active Directory setup.
    @@ -449,16 +448,16 @@ eo: attribute_help_text: attribute_name: 'Atributo' help_text: 'Helpteksto' - auth_source: - account: "Konto" - attr_firstname: "Atribuo Nomo" - attr_lastname: "Atribuo Familinomo" - attr_login: "Atribuo uzantnomo" - attr_mail: "Atribuo retpoŝto" - base_dn: "Bazo DN" - host: "Gastiganto" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" + attr_login: "Username attribute" + attr_mail: "Email attribute" + base_dn: "Base DN" + host: "Host" onthefly: "Automatic user creation" - port: "Pordo" + port: "Port" tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "Deponejo" @@ -557,7 +556,7 @@ eo: color: "Koloro" user: admin: "Administranto" - auth_source: "Aŭtentigaj reĝimoj" + ldap_auth_source: "LDAP connection" current_password: "Nuna pasvorto" force_password_change: "Enforce password change on next login" language: "Lingvo" @@ -678,7 +677,7 @@ eo: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "la longeco estas malĝusta (devus esti %{count} signoj)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -900,8 +899,8 @@ eo: description: "'Password confirmation' should match the input in the 'New password' field." status: invalid_on_create: "is not a valid status for new users." - auth_source: - error_not_found: "ne trovita" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "Please choose at least one user or group." role_blank: "ĝi bezonas esti atribuita." @@ -1436,6 +1435,7 @@ eo: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1450,6 +1450,8 @@ eo: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Agordgvidilo' get_in_touch: "You have questions? Get in touch with us." @@ -1494,6 +1496,13 @@ eo: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1562,9 +1571,9 @@ eo: label_attachment_plural: "Dosieroj" label_attribute: "Atributo" label_attribute_plural: "Atributoj" - label_auth_source: "Aŭtentigaj reĝimoj" - label_auth_source_new: "Nova aŭtentiga reĝimo" - label_auth_source_plural: "Aŭtentigaj reĝimoj" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Aŭtentigo" label_available_project_work_package_categories: 'disponeblaj laborpakaĵaj kategorioj' label_available_project_work_package_types: 'Disponeblaj laborpakaĵaj tipoj' @@ -1730,7 +1739,7 @@ eo: label_hierarchy_leaf: "Hierarkia folio" label_home: "Ĉefpaĝo" label_subject_or_id: "Temo aŭ ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Leĝa atentigo" label_in: "en" label_in_less_than: "en malpli ol" @@ -2087,10 +2096,7 @@ eo: label_global_role: "Ĉiea rolo" label_not_changeable: "(ne modifebla)" label_global: "Ĉiea" - auth_source: - using_abstract_auth_source: "Can't use an abstract authentication source." - ldap_error: "LDAP-Error: %{error_message}" - ldap_auth_failed: "Could not authenticate at the LDAP-Server." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Error executing the macro %{macro_name}" macro_unavailable: "Macro %{macro_name} cannot be displayed." macros: @@ -2279,7 +2285,7 @@ eo: present_access_key_value: "Your %{key_name} is: %{value}" notice_automatic_set_of_standard_type: "Set standard type automatically." notice_logged_out: "You have been logged out." - notice_wont_delete_auth_source: The authentication mode cannot be deleted as long as there are still users using it. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields. The project is invalid: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2766,12 +2772,16 @@ eo: text_work_packages_destroy_confirmation: "Are you sure you want to delete the selected work package(s)?" text_work_packages_ref_in_commit_messages: "Referencing and fixing work packages in commit messages" text_journal_added: "%{label} %{value} added" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} updated" text_journal_changed_with_diff: "%{label} changed (%{link})" text_journal_deleted: "%{label} deleted (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} deleted (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} set to %{value}" text_journal_set_with_diff: "%{label} set (%{link})" diff --git a/config/locales/crowdin/es.yml b/config/locales/crowdin/es.yml index 32ea8f454cc..d48d716b24c 100644 --- a/config/locales/crowdin/es.yml +++ b/config/locales/crowdin/es.yml @@ -99,15 +99,14 @@ es: edit: "Editar texto de ayuda para %{attribute_caption}" enterprise: description: 'Proporcione información adicional para atributos (incl. campos personalizados) de los paquetes de trabajo y proyectos. Los textos de ayuda se muestran cuando los usuarios hacen clic en el símbolo de signo de interrogación junto a los campos correspondientes en proyectos y paquetes de trabajo.' - auth_sources: - index: - no_results_content_title: Actualmente no hay modos de autenticación. - no_results_content_text: Crear un nuevo modo de autenticación background_jobs: status: error_requeue: "El trabajo experimentó un error pero se está reintentando. El error fue: %{message}" cancelled_due_to: "El trabajo ha sido cancelado debido al error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | Este formulario LDAP requiere conocimientos técnicos para la configuración de su LDAP / Directorio Activo
    Visite nuestra documentación para obtener instrucciones detalladas. @@ -446,17 +445,17 @@ es: attribute_help_text: attribute_name: 'Atributo' help_text: 'Texto de ayuda' - auth_source: - account: "Cuenta" - attr_firstname: "Atributo Nombre" - attr_lastname: "Atributo Apellido" - attr_login: "Atributo de nombre de usuario" - attr_mail: "Atributo Correo electrónico" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" + attr_login: "Username attribute" + attr_mail: "Email attribute" base_dn: "Base DN" host: "Host" - onthefly: "Creación automática de usuarios" - port: "Puerto" - tls_certificate_string: "Certificado SSL del servidor LDAP" + onthefly: "Automatic user creation" + port: "Port" + tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "Repositorio" comment: @@ -554,7 +553,7 @@ es: color: "Color" user: admin: "Administrador" - auth_source: "Modos de autentificación" + ldap_auth_source: "LDAP connection" current_password: "Contraseña actual" force_password_change: "Aplicar el cambio de contraseña en el proximo inicio de sesión" language: "Idioma" @@ -675,10 +674,10 @@ es: no está proveyendo un "Contexto Seguro". Utiliza HTTPS o una dirección loopack, como un localhost. wrong_length: "la longitud es incorrecta (debe ser %{count} caracteres)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: - invalid_certificate: "El certificado SSL proporcionado no es válido: %{additional_message}" + invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" format: "%{message}" attachment: attributes: @@ -897,8 +896,8 @@ es: description: "'La confirmación de la contraseña' debe coincidir con la ingresada en el campo 'Nueva contraseña'." status: invalid_on_create: "no es un estado válido para nuevos usuarios." - auth_source: - error_not_found: "no encontrado" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "Por favor seleccione al menos un usuario o grupo." role_blank: "necesita ser asignado." @@ -1433,6 +1432,7 @@ es: changes_retracted: "Se deshicieron los cambios." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1447,6 +1447,8 @@ es: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Guía de configuración' get_in_touch: "¿Tiene alguna pregunta? Póngase en contacto con nosotros." @@ -1491,6 +1493,13 @@ es: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Enviar notificaciones para esta acción" work_packages: @@ -1559,9 +1568,9 @@ es: label_attachment_plural: "Archivos" label_attribute: "Atributo" label_attribute_plural: "Atributos" - label_auth_source: "Modo de autentificación" - label_auth_source_new: "Nuevo modo de autenticaficación" - label_auth_source_plural: "Modos de autentificación" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Autentificación" label_available_project_work_package_categories: 'Categorias de paquetes de trabjo disponibles' label_available_project_work_package_types: 'Tipos de paquete de trabajo disponibles' @@ -1727,7 +1736,7 @@ es: label_hierarchy_leaf: "Hoja de jerarquía" label_home: "Inicio" label_subject_or_id: "Asunto o ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Aviso legal" label_in: "en" label_in_less_than: "en menos de" @@ -2084,10 +2093,7 @@ es: label_global_role: "Rol global" label_not_changeable: "(no modificable)" label_global: "Global" - auth_source: - using_abstract_auth_source: "No puede utilizar una fuente de autenticación abstracta." - ldap_error: "Error de LDAP: %{error_message}" - ldap_auth_failed: "No puede autenticarse en el servidor LDAP." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Error ejecutando el macro %{macro_name}" macro_unavailable: "Macro %{macro_name} no puede ser mostrado." macros: @@ -2275,7 +2281,7 @@ es: present_access_key_value: "Su %{key_name} es: %{value}" notice_automatic_set_of_standard_type: "Establecer tipo estándar automáticamente." notice_logged_out: "Has cerrado la sesion." - notice_wont_delete_auth_source: El modo de autenticación no puede ser borrado mientras haya usuarios utilizándolo. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "No se pueden actualizar campos personalizados disponibles del proyecto. El proyecto es inválido: %{errors}" notice_attachment_migration_wiki_page: > Esta página se generó automáticamente al actualizar OpenProject. Contiene todos los datos adjuntos que estaban asociados anteriormente al %{container_type} "%{container_name}". @@ -2762,12 +2768,16 @@ es: text_work_packages_destroy_confirmation: "¿Esta usted seguro que desea eliminar paquete(s) de trabajo seleccionado(s)?" text_work_packages_ref_in_commit_messages: "Referenciando y reparando paquetes de trabajo en mensajes de commit" text_journal_added: "%{label} %{value} añadido" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} se cambió de %{old} %{linebreak}a %{new}" text_journal_changed_no_detail: "%{label} actualizado" text_journal_changed_with_diff: "%{label} modificado (%{link})" text_journal_deleted: "%{label} eliminado (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} eliminado (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} establecido al %{value}" text_journal_set_with_diff: "%{label} fijado (%{link})" diff --git a/config/locales/crowdin/et.yml b/config/locales/crowdin/et.yml index 72472a088dc..6505fa4fb12 100644 --- a/config/locales/crowdin/et.yml +++ b/config/locales/crowdin/et.yml @@ -99,15 +99,14 @@ et: edit: "Edit help text for %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: There are currently no authentication modes. - no_results_content_text: Create a new authentication mode background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | This LDAP form requires technical knowledge of your LDAP / Active Directory setup.
    @@ -449,13 +448,13 @@ et: attribute_help_text: attribute_name: 'Atribuut' help_text: 'Abitekst' - auth_source: - account: "Konto" - attr_firstname: "Eesnimi" - attr_lastname: "Perekonnanimi" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" attr_login: "Username attribute" - attr_mail: "E-posti aadress" - base_dn: "Põhiline domeeninimi" + attr_mail: "Email attribute" + base_dn: "Base DN" host: "Host" onthefly: "Automatic user creation" port: "Port" @@ -557,7 +556,7 @@ et: color: "Värv" user: admin: "Administraator" - auth_source: "Autentimise viis" + ldap_auth_source: "LDAP connection" current_password: "Praegune parool" force_password_change: "Sunni parooli vahetada järgmisel sisselogimisel" language: "Keel" @@ -678,7 +677,7 @@ et: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "is the wrong length (should be %{count} characters)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -900,7 +899,7 @@ et: description: "'Password confirmation' should match the input in the 'New password' field." status: invalid_on_create: "is not a valid status for new users." - auth_source: + ldap_auth_source: error_not_found: "not found" member: principal_blank: "Vali vähemalt üks kasutaja või grupp." @@ -1436,6 +1435,7 @@ et: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1450,6 +1450,8 @@ et: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Configuration guide' get_in_touch: "You have questions? Get in touch with us." @@ -1494,6 +1496,13 @@ et: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1562,9 +1571,9 @@ et: label_attachment_plural: "Failid" label_attribute: "Atribuut" label_attribute_plural: "Atribuudid" - label_auth_source: "Autentimise viis" - label_auth_source_new: "Uus autentimise viis" - label_auth_source_plural: "Autentimise viisid" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Autentimine" label_available_project_work_package_categories: 'Available work package categories' label_available_project_work_package_types: 'Available work package types' @@ -1730,7 +1739,7 @@ et: label_hierarchy_leaf: "Hierarchy leaf" label_home: "Avaleht" label_subject_or_id: "Subject or ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "sisaldub hulgas" label_in_less_than: "on väiksem kui" @@ -2087,10 +2096,7 @@ et: label_global_role: "Globaalne roll" label_not_changeable: "(pole muudetav)" label_global: "Üldine" - auth_source: - using_abstract_auth_source: "Can't use an abstract authentication source." - ldap_error: "LDAP-Error: %{error_message}" - ldap_auth_failed: "Could not authenticate at the LDAP-Server." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Error executing the macro %{macro_name}" macro_unavailable: "Macro %{macro_name} cannot be displayed." macros: @@ -2279,7 +2285,7 @@ et: present_access_key_value: "Your %{key_name} is: %{value}" notice_automatic_set_of_standard_type: "Vaikimisi tüüp valitakse automaatselt." notice_logged_out: "You have been logged out." - notice_wont_delete_auth_source: The authentication mode cannot be deleted as long as there are still users using it. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields. The project is invalid: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2766,12 +2772,16 @@ et: text_work_packages_destroy_confirmation: "Oled Sa kindel oma soovis valitud teema(d) kustutada?" text_work_packages_ref_in_commit_messages: "Teemadele ja parandustele viitamine sissekannete märkustes" text_journal_added: "%{label} %{value} lisatud" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} uuendatud" text_journal_changed_with_diff: "%{label} uuendatud (%{link})" text_journal_deleted: "%{label} kustutatud (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} kustutatud (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} on nüüd %{value}" text_journal_set_with_diff: "%{label} määratud (%{link})" diff --git a/config/locales/crowdin/eu.yml b/config/locales/crowdin/eu.yml index 4e8c2e8a34c..45091d969ad 100644 --- a/config/locales/crowdin/eu.yml +++ b/config/locales/crowdin/eu.yml @@ -99,15 +99,14 @@ eu: edit: "Edit help text for %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: There are currently no authentication modes. - no_results_content_text: Create a new authentication mode background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | This LDAP form requires technical knowledge of your LDAP / Active Directory setup.
    @@ -449,7 +448,7 @@ eu: attribute_help_text: attribute_name: 'Attribute' help_text: 'Help text' - auth_source: + ldap_auth_source: account: "Account" attr_firstname: "Firstname attribute" attr_lastname: "Lastname attribute" @@ -557,7 +556,7 @@ eu: color: "Color" user: admin: "Administrator" - auth_source: "Authentication mode" + ldap_auth_source: "LDAP connection" current_password: "Current password" force_password_change: "Enforce password change on next login" language: "Language" @@ -678,7 +677,7 @@ eu: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "is the wrong length (should be %{count} characters)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -900,7 +899,7 @@ eu: description: "'Password confirmation' should match the input in the 'New password' field." status: invalid_on_create: "is not a valid status for new users." - auth_source: + ldap_auth_source: error_not_found: "not found" member: principal_blank: "Please choose at least one user or group." @@ -1436,6 +1435,7 @@ eu: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1450,6 +1450,8 @@ eu: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Configuration guide' get_in_touch: "You have questions? Get in touch with us." @@ -1494,6 +1496,13 @@ eu: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1562,9 +1571,9 @@ eu: label_attachment_plural: "Files" label_attribute: "Attribute" label_attribute_plural: "Attributes" - label_auth_source: "Authentication mode" - label_auth_source_new: "New authentication mode" - label_auth_source_plural: "Authentication modes" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Authentication" label_available_project_work_package_categories: 'Available work package categories' label_available_project_work_package_types: 'Available work package types' @@ -1730,7 +1739,7 @@ eu: label_hierarchy_leaf: "Hierarchy leaf" label_home: "Home" label_subject_or_id: "Subject or ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "in" label_in_less_than: "in less than" @@ -2087,10 +2096,7 @@ eu: label_global_role: "Global Role" label_not_changeable: "(not changeable)" label_global: "Global" - auth_source: - using_abstract_auth_source: "Can't use an abstract authentication source." - ldap_error: "LDAP-Error: %{error_message}" - ldap_auth_failed: "Could not authenticate at the LDAP-Server." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Error executing the macro %{macro_name}" macro_unavailable: "Macro %{macro_name} cannot be displayed." macros: @@ -2279,7 +2285,7 @@ eu: present_access_key_value: "Your %{key_name} is: %{value}" notice_automatic_set_of_standard_type: "Set standard type automatically." notice_logged_out: "You have been logged out." - notice_wont_delete_auth_source: The authentication mode cannot be deleted as long as there are still users using it. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields. The project is invalid: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2766,12 +2772,16 @@ eu: text_work_packages_destroy_confirmation: "Are you sure you want to delete the selected work package(s)?" text_work_packages_ref_in_commit_messages: "Referencing and fixing work packages in commit messages" text_journal_added: "%{label} %{value} added" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} updated" text_journal_changed_with_diff: "%{label} changed (%{link})" text_journal_deleted: "%{label} deleted (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} deleted (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} set to %{value}" text_journal_set_with_diff: "%{label} set (%{link})" diff --git a/config/locales/crowdin/fa.yml b/config/locales/crowdin/fa.yml index 6175f47f458..041cb69f52f 100644 --- a/config/locales/crowdin/fa.yml +++ b/config/locales/crowdin/fa.yml @@ -99,15 +99,14 @@ fa: edit: "Edit help text for %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: There are currently no authentication modes. - no_results_content_text: Create a new authentication mode background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | This LDAP form requires technical knowledge of your LDAP / Active Directory setup.
    @@ -449,16 +448,16 @@ fa: attribute_help_text: attribute_name: 'Attribute' help_text: 'متن راهنما' - auth_source: - account: "حساب کاربری" - attr_firstname: "مشخصه نام" - attr_lastname: "مشخصه نام خانوادگی" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" attr_login: "Username attribute" - attr_mail: "ویژگی ایمیل" - base_dn: "پایه DN" - host: "میزبان" + attr_mail: "Email attribute" + base_dn: "Base DN" + host: "Host" onthefly: "Automatic user creation" - port: "پورت" + port: "Port" tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "Repository" @@ -557,7 +556,7 @@ fa: color: "رنگ" user: admin: "مدیر" - auth_source: "حالت تأیید اعتبار" + ldap_auth_source: "LDAP connection" current_password: "Current password" force_password_change: "اجبار برای تغییر گذرواژه در ورود بعدی" language: "زبان" @@ -678,7 +677,7 @@ fa: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "is the wrong length (should be %{count} characters)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -900,8 +899,8 @@ fa: description: "'Password confirmation' should match the input in the 'New password' field." status: invalid_on_create: "is not a valid status for new users." - auth_source: - error_not_found: "یافت نشد" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "لطفا حداقل یک کاربر یا گروه را انتخاب کنید." role_blank: "need to be assigned." @@ -1436,6 +1435,7 @@ fa: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1450,6 +1450,8 @@ fa: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'راهنمای پیکربندی' get_in_touch: "You have questions? Get in touch with us." @@ -1494,6 +1496,13 @@ fa: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1562,9 +1571,9 @@ fa: label_attachment_plural: "Files" label_attribute: "Attribute" label_attribute_plural: "Attributes" - label_auth_source: "حالت تأیید اعتبار" - label_auth_source_new: "New authentication mode" - label_auth_source_plural: "Authentication modes" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Authentication" label_available_project_work_package_categories: 'Available work package categories' label_available_project_work_package_types: 'Available work package types' @@ -1730,7 +1739,7 @@ fa: label_hierarchy_leaf: "Hierarchy leaf" label_home: "Home" label_subject_or_id: "Subject or ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "in" label_in_less_than: "in less than" @@ -2087,10 +2096,7 @@ fa: label_global_role: "نقش عمومی" label_not_changeable: "(not changeable)" label_global: "عمومی" - auth_source: - using_abstract_auth_source: "Can't use an abstract authentication source." - ldap_error: "LDAP-Error: %{error_message}" - ldap_auth_failed: "Could not authenticate at the LDAP-Server." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Error executing the macro %{macro_name}" macro_unavailable: "Macro %{macro_name} cannot be displayed." macros: @@ -2279,7 +2285,7 @@ fa: present_access_key_value: "Your %{key_name} is: %{value}" notice_automatic_set_of_standard_type: "Set standard type automatically." notice_logged_out: "You have been logged out." - notice_wont_delete_auth_source: The authentication mode cannot be deleted as long as there are still users using it. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields. The project is invalid: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2766,12 +2772,16 @@ fa: text_work_packages_destroy_confirmation: "Are you sure you want to delete the selected work package(s)?" text_work_packages_ref_in_commit_messages: "Referencing and fixing work packages in commit messages" text_journal_added: "%{label} %{value} added" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} updated" text_journal_changed_with_diff: "%{label} changed (%{link})" text_journal_deleted: "%{label} deleted (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} deleted (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} set to %{value}" text_journal_set_with_diff: "%{label} set (%{link})" diff --git a/config/locales/crowdin/fi.yml b/config/locales/crowdin/fi.yml index cdd419ed376..87c195b8db5 100644 --- a/config/locales/crowdin/fi.yml +++ b/config/locales/crowdin/fi.yml @@ -99,15 +99,14 @@ fi: edit: "Muokkaa aputeksiä ominaisuudelle %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: Tällä hetkellä ei ole todennustiloja. - no_results_content_text: Luo uusi todennustila background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | This LDAP form requires technical knowledge of your LDAP / Active Directory setup.
    @@ -449,16 +448,16 @@ fi: attribute_help_text: attribute_name: 'Määre' help_text: 'Help text' - auth_source: - account: "Käyttäjätili" - attr_firstname: "Etunimi-tietue" - attr_lastname: "Sukunimi-tietue" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" attr_login: "Username attribute" - attr_mail: "Sähköposti-tietue" + attr_mail: "Email attribute" base_dn: "Base DN" - host: "Isäntä" + host: "Host" onthefly: "Automatic user creation" - port: "Portti" + port: "Port" tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "Tietovarasto" @@ -557,7 +556,7 @@ fi: color: "Väri" user: admin: "Järjestelmänvalvoja" - auth_source: "Todennustila" + ldap_auth_source: "LDAP connection" current_password: "Nykyinen salasana" force_password_change: "Pakota salasanan vaihto seuraavan kirjautumisen yhteydessä" language: "Kieli" @@ -678,7 +677,7 @@ fi: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "on väärän pituinen (täytyy olla täsmälleen %{count} merkkiä)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -900,8 +899,8 @@ fi: description: "\"Salasanan vahvistus\" -kentän arvon tulee olla identtinen \"Uusi salasana\" -kentän arvon kanssa." status: invalid_on_create: "ei ole kelvollinen tila uusille käyttäjille." - auth_source: - error_not_found: "ei löydetty" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "Valitse vähintään yksi käyttäjä tai ryhmä." role_blank: "need to be assigned." @@ -1436,6 +1435,7 @@ fi: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1450,6 +1450,8 @@ fi: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Configuration guide' get_in_touch: "You have questions? Get in touch with us." @@ -1494,6 +1496,13 @@ fi: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1562,9 +1571,9 @@ fi: label_attachment_plural: "Tiedostot" label_attribute: "Määre" label_attribute_plural: "Määreet" - label_auth_source: "Todennustila" - label_auth_source_new: "Uusi kirjautumistapa" - label_auth_source_plural: "Kirjautumistavat" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Kirjautuminen" label_available_project_work_package_categories: 'Olemassa olevat tehtäväluokat' label_available_project_work_package_types: 'Olemassa olevat tehtävätyypit' @@ -1730,7 +1739,7 @@ fi: label_hierarchy_leaf: "Hierarchy leaf" label_home: "Koti" label_subject_or_id: "Aihe tai ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "tässä" label_in_less_than: "pienempi kuin" @@ -2087,10 +2096,7 @@ fi: label_global_role: "Yleinen rooli" label_not_changeable: "(ei vaihdettavissa)" label_global: "Yleinen" - auth_source: - using_abstract_auth_source: "Voi käyttää abstrakti autentikointi lähde." - ldap_error: "LDAP-Error: %{error_message}" - ldap_auth_failed: "Could not authenticate at the LDAP-Server." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Error executing the macro %{macro_name}" macro_unavailable: "Macro %{macro_name} cannot be displayed." macros: @@ -2279,7 +2285,7 @@ fi: present_access_key_value: "Your %{key_name} is: %{value}" notice_automatic_set_of_standard_type: "Set standard type automatically." notice_logged_out: "You have been logged out." - notice_wont_delete_auth_source: Todennustilaa ei voi poistaa koska sillä on vielä käyttäjiä. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "Et voi päivittää projektin käytettävissä olevia mukautettuja kenttiä. Projekti on virheellinen: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2766,12 +2772,16 @@ fi: text_work_packages_destroy_confirmation: "Are you sure you want to delete the selected work package(s)?" text_work_packages_ref_in_commit_messages: "Vertailemalla ja korjaus toimia paketit sitoutua viestejä" text_journal_added: "%{label} %{value} lisätty" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} päivitetty" text_journal_changed_with_diff: "%{label} muutettu (%{link})" text_journal_deleted: "%{label} poistettu (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} deleted (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} muutettu %{value}" text_journal_set_with_diff: "%{label} set (%{link})" diff --git a/config/locales/crowdin/fil.yml b/config/locales/crowdin/fil.yml index f70b1e30130..83d5080f670 100644 --- a/config/locales/crowdin/fil.yml +++ b/config/locales/crowdin/fil.yml @@ -99,15 +99,14 @@ fil: edit: "I-edit na tulong na teksto para sa %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: Sa kasalukuyan ay walang mga authentication mode. - no_results_content_text: Gumawa ng bagong authentication mode background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | This LDAP form requires technical knowledge of your LDAP / Active Directory setup.
    @@ -449,13 +448,13 @@ fil: attribute_help_text: attribute_name: 'Katangian' help_text: 'Tekstong tulong' - auth_source: - account: "Akawnt" - attr_firstname: "Ang katangian ng unang pangalan" - attr_lastname: "Ang huling katangian ng apelyido" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" attr_login: "Username attribute" - attr_mail: "Ang katangian ng Email" - base_dn: "Ang Base DN" + attr_mail: "Email attribute" + base_dn: "Base DN" host: "Host" onthefly: "Automatic user creation" port: "Port" @@ -557,7 +556,7 @@ fil: color: "Kulay" user: admin: "Tagapangasiwa" - auth_source: "Mode ng pagpapatunay" + ldap_auth_source: "LDAP connection" current_password: "Kasulukuyang password" force_password_change: "Ipatupad ang password na ibahin sa susunod na login" language: "Linggwahe" @@ -678,7 +677,7 @@ fil: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "ay ang maling haba (dapat ay %{count} ang mga karakter)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -900,8 +899,8 @@ fil: description: "'Kompirmasyon ng password' ay dapat tugma sa input ng 'Bagong password' na patlang." status: invalid_on_create: "ay hindi isang balidong estado para sa mga bagong gumagamit." - auth_source: - error_not_found: "hindi nakita" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "Mangayring pumili na kahit isang gumagamit o grupo." role_blank: "need to be assigned." @@ -1436,6 +1435,7 @@ fil: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1450,6 +1450,8 @@ fil: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Gabay ng kumpigurasyon' get_in_touch: "You have questions? Get in touch with us." @@ -1494,6 +1496,13 @@ fil: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1562,9 +1571,9 @@ fil: label_attachment_plural: "Mga file" label_attribute: "Katangian" label_attribute_plural: "Mga katangian" - label_auth_source: "Mode ng pagpapatunay" - label_auth_source_new: "Bagong authentication mode" - label_auth_source_plural: "Ang mga mode ng pagpapatunay" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Pagpapatunay" label_available_project_work_package_categories: 'Ang mga kategorya ng magagamit na work package' label_available_project_work_package_types: 'Available work package types' @@ -1730,7 +1739,7 @@ fil: label_hierarchy_leaf: "Hierarchy leaf" label_home: "Tahanan" label_subject_or_id: "Subject or ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "sa" label_in_less_than: "mas mababa kaysa" @@ -2087,10 +2096,7 @@ fil: label_global_role: "Global Role" label_not_changeable: "(not changeable)" label_global: "Global" - auth_source: - using_abstract_auth_source: "Hindi magamit ang isang abstrak na pagpapatunay ng pinagmulan." - ldap_error: "LDAP-Error: %{error_message}" - ldap_auth_failed: "Hindi maarig mapatunayan sa LDAP-Server." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "May mali habang isinasagawa ang macro %{macro_name}" macro_unavailable: "Macro %{macro_name} hindi naka-display." macros: @@ -2279,7 +2285,7 @@ fil: present_access_key_value: "Ang iyong %{key_name} ay: %{value}" notice_automatic_set_of_standard_type: "Magtakda ng automatikong pamantayang uri." notice_logged_out: "Ikaw ay naka-log out na." - notice_wont_delete_auth_source: Ang authentication mode ay hindi pwedeng burahin hanggang meron pang user ang gumagamit nito. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "Hindi mo pwedeng i-update ang mga patlang ng project's available custom. Ang proyekto ay hindi balido: %{errors}" notice_attachment_migration_wiki_page: > Ang pahinang ito ay awtomatikong binuo sa panahon ng pag-update sa OpenProject. Ito ay naglalaman ng lahat na mga nakakalakip sa nakaraang nauugnay sa %{container_type} "%{container_name}". @@ -2764,12 +2770,16 @@ fil: text_work_packages_destroy_confirmation: "Sigurado ka ba na gusto mong burahin ang napiling work package?" text_work_packages_ref_in_commit_messages: "Pagsangguni at pagsaayos ng work package sa mga isinagawang mensahe" text_journal_added: "%{label}%{value} idinagdag" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} ay naka-update" text_journal_changed_with_diff: "%{label} binagko (%{link})" text_journal_deleted: "%{label} binura (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} binura (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} itinakda sa %{value}" text_journal_set_with_diff: "%{label} itinakda (%{link})" diff --git a/config/locales/crowdin/fr.yml b/config/locales/crowdin/fr.yml index 388a16dd033..7316821e653 100644 --- a/config/locales/crowdin/fr.yml +++ b/config/locales/crowdin/fr.yml @@ -99,15 +99,14 @@ fr: edit: "Modifier le texte d’aide pour %{attribute_caption}" enterprise: description: 'Fournissez des informations supplémentaires pour les attributs (y compris les champs personnalisés) des lots de travaux et des projets. Les textes d''aide sont affichés lorsque les utilisateurs cliquent sur le symbole point d''interrogation à côté des champs de saisie dans les projets et les lots de travaux.' - auth_sources: - index: - no_results_content_title: Il n'y a actuellement aucun mode d'authentification. - no_results_content_text: Créer un nouveau mode d'authentification background_jobs: status: error_requeue: "La tâche a rencontré une erreur mais réessaie. L'erreur était : %{message}" cancelled_due_to: "La tâche a été annulée en raison d'une erreur : %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | Ce formulaire LDAP nécessite une connaissance technique de votre configuration LDAP / Active Directory.
    @@ -449,17 +448,17 @@ fr: attribute_help_text: attribute_name: 'Attribut' help_text: 'Texte d’aide' - auth_source: - account: "Compte" - attr_firstname: "Attribut Prénom (Firstname)" - attr_lastname: "Attribut Nom de famille (Lastname)" - attr_login: "Attribut nom d'utilisateur" - attr_mail: "Attribut Courriel (Email)" - base_dn: "DN de base" - host: "Hôte" - onthefly: "Création automatique d'un utilisateur" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" + attr_login: "Username attribute" + attr_mail: "Email attribute" + base_dn: "Base DN" + host: "Host" + onthefly: "Automatic user creation" port: "Port" - tls_certificate_string: "Certificat SSL du serveur LDAP" + tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "Référentiel" comment: @@ -557,7 +556,7 @@ fr: color: "Couleur" user: admin: "Administrateur" - auth_source: "Mode d'authentification" + ldap_auth_source: "LDAP connection" current_password: "Mot de passe actuel" force_password_change: "Obliger l'utilisateur à changer de mot de passe à la prochaine connexion" language: "Langue" @@ -678,10 +677,10 @@ fr: ne fournit pas de « Contexte sécurisé ». Utilisez soit HTTPS, soit une adresse de bouclage, comme localhost. wrong_length: "est de mauvaise longueur (devrait être %{count} caractères)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: - invalid_certificate: "Le certificat SSL fourni est invalide : %{additional_message}" + invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" format: "%{message}" attachment: attributes: @@ -900,8 +899,8 @@ fr: description: "La confirmation du mot de passe doit correspondre à celui saisi dans le champ “Nouveau mot de passe”." status: invalid_on_create: "n’est pas un statut valide pour les nouveaux utilisateurs." - auth_source: - error_not_found: "non trouvé" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "Veuillez choisir au moins un utilisateur ou un groupe." role_blank: "doit être assigné." @@ -1436,6 +1435,7 @@ fr: changes_retracted: "Les modifications ont été retirées." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1450,6 +1450,8 @@ fr: dates: working: "%{date} est maintenant un jour ouvrable" non_working: "%{date} est maintenant un jour non ouvrable" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Guide de configuration' get_in_touch: "Vous avez des questions ? Contactez-nous." @@ -1494,6 +1496,13 @@ fr: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Envoyer des notifications pour cette action" work_packages: @@ -1562,9 +1571,9 @@ fr: label_attachment_plural: "Fichiers" label_attribute: "Attribut" label_attribute_plural: "Attributs" - label_auth_source: "Mode d'authentification" - label_auth_source_new: "Nouveau mode d'authentification" - label_auth_source_plural: "Modes d'authentification" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Authentification" label_available_project_work_package_categories: 'Catégories de lots de travaux disponibles' label_available_project_work_package_types: 'Types de paquet de travail disponible' @@ -1730,7 +1739,7 @@ fr: label_hierarchy_leaf: "Feuille de hiérarchie" label_home: "Accueil" label_subject_or_id: "Objet ou ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Mentions légales" label_in: "dans" label_in_less_than: "dans moins de" @@ -2087,10 +2096,7 @@ fr: label_global_role: "Rôle global" label_not_changeable: "(non modifiable)" label_global: "Global" - auth_source: - using_abstract_auth_source: "Impossible d'utiliser une source d'authentification abstraite." - ldap_error: "Erreur LDAP : %{error_message}" - ldap_auth_failed: "Ne peut s'authentifier sur le serveur LDAP." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Erreur lors de l'exécution de la macro %{macro_name}" macro_unavailable: "La macro %{macro_name} ne peut être affichée." macros: @@ -2279,7 +2285,7 @@ fr: present_access_key_value: "Votre %{key_name} est : %{value}" notice_automatic_set_of_standard_type: "Fixer le type standard automatiquement." notice_logged_out: "Vous avez été déconnecté." - notice_wont_delete_auth_source: Cette méthode d'authentification ne peut être supprimée tant qu'elle est utilisée par un utilisateur. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "Vous ne pouvez pas mettre à jour les champs personnalisés disponibles du projet. Le projet n’est pas valide : %{errors}" notice_attachment_migration_wiki_page: > Cette page a été générée automatiquement durant la mise à jour de OpenProject. Il contient toutes les pièces jointes précédemment associées à la %{container_type} « %{container_name} ». @@ -2766,12 +2772,16 @@ fr: text_work_packages_destroy_confirmation: "Êtes-vous sûr de vouloir supprimer le(s) lot(s) de travaux sélectionné(s) ?" text_work_packages_ref_in_commit_messages: "Referencer et réparer les Lot(s) de Travaux dans les messages « commit »" text_journal_added: "%{label} %{value} ajouté" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changé de %{old} %{linebreak}à %{new}" text_journal_changed_no_detail: "%{label} mis à jour" text_journal_changed_with_diff: "%{label} changé (%{link})" text_journal_deleted: "%{label} supprimé (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} supprimé (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} mis à %{value}" text_journal_set_with_diff: "%{label} mis (%{link})" diff --git a/config/locales/crowdin/he.yml b/config/locales/crowdin/he.yml index 75affdde6e9..ecd6f9b2ff2 100644 --- a/config/locales/crowdin/he.yml +++ b/config/locales/crowdin/he.yml @@ -99,15 +99,14 @@ he: edit: "Edit help text for %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: There are currently no authentication modes. - no_results_content_text: Create a new authentication mode background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | This LDAP form requires technical knowledge of your LDAP / Active Directory setup.
    @@ -451,14 +450,14 @@ he: attribute_help_text: attribute_name: 'תכונה' help_text: 'Help text' - auth_source: - account: "חשבון" - attr_firstname: "תכונת שם פרטי" - attr_lastname: "תכונת שם משפחה" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" attr_login: "Username attribute" - attr_mail: "תכונת דואר אלקטרוני" - base_dn: "תכנון אלמנט" - host: "שרת" + attr_mail: "Email attribute" + base_dn: "Base DN" + host: "Host" onthefly: "Automatic user creation" port: "Port" tls_certificate_string: "LDAP server SSL certificate" @@ -559,7 +558,7 @@ he: color: "צבע" user: admin: "מנהל מערכת" - auth_source: "מצב אימות" + ldap_auth_source: "LDAP connection" current_password: "Current password" force_password_change: "לאכוף שינוי סיסמה בכניסה הבאה" language: "שפה" @@ -680,7 +679,7 @@ he: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "is the wrong length (should be %{count} characters)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -904,7 +903,7 @@ he: description: "'סיסמה' צריך להתאים הקלט 'סיסמה חדשה' שדה." status: invalid_on_create: "is not a valid status for new users." - auth_source: + ldap_auth_source: error_not_found: "not found" member: principal_blank: "נא לבחור לפחות משתמש אחד או קבוצה." @@ -1474,6 +1473,7 @@ he: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1488,6 +1488,8 @@ he: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Configuration guide' get_in_touch: "You have questions? Get in touch with us." @@ -1532,6 +1534,13 @@ he: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1600,9 +1609,9 @@ he: label_attachment_plural: "קבצים" label_attribute: "תכונה" label_attribute_plural: "תכונות" - label_auth_source: "מצב אימות" - label_auth_source_new: "מצב אימות חדש" - label_auth_source_plural: "מצבי אימות" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "אימות" label_available_project_work_package_categories: 'Available work package categories' label_available_project_work_package_types: 'Available work package types' @@ -1768,7 +1777,7 @@ he: label_hierarchy_leaf: "Hierarchy leaf" label_home: "דף הבית" label_subject_or_id: "Subject or ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "ב" label_in_less_than: "in less than" @@ -2125,10 +2134,7 @@ he: label_global_role: "Global Role" label_not_changeable: "(not changeable)" label_global: "Global" - auth_source: - using_abstract_auth_source: "Can't use an abstract authentication source." - ldap_error: "LDAP-Error: %{error_message}" - ldap_auth_failed: "Could not authenticate at the LDAP-Server." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Error executing the macro %{macro_name}" macro_unavailable: "Macro %{macro_name} cannot be displayed." macros: @@ -2319,7 +2325,7 @@ he: present_access_key_value: "Your %{key_name} is: %{value}" notice_automatic_set_of_standard_type: "Set standard type automatically." notice_logged_out: "You have been logged out." - notice_wont_delete_auth_source: The authentication mode cannot be deleted as long as there are still users using it. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields. The project is invalid: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2808,12 +2814,16 @@ he: text_work_packages_destroy_confirmation: "Are you sure you want to delete the selected work package(s)?" text_work_packages_ref_in_commit_messages: "Referencing and fixing work packages in commit messages" text_journal_added: "%{label} %{value} added" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} updated" text_journal_changed_with_diff: "%{label} changed (%{link})" text_journal_deleted: "%{label} deleted (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} deleted (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} set to %{value}" text_journal_set_with_diff: "%{label} set (%{link})" diff --git a/config/locales/crowdin/hi.yml b/config/locales/crowdin/hi.yml index 295267617e2..c98bf0f7457 100644 --- a/config/locales/crowdin/hi.yml +++ b/config/locales/crowdin/hi.yml @@ -99,15 +99,14 @@ hi: edit: "Edit help text for %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: There are currently no authentication modes. - no_results_content_text: Create a new authentication mode background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | This LDAP form requires technical knowledge of your LDAP / Active Directory setup.
    @@ -447,16 +446,16 @@ hi: attribute_help_text: attribute_name: 'गुण' help_text: 'Help text' - auth_source: - account: "खाता" - attr_firstname: "पहला-नाम गुण" - attr_lastname: "कुल-नाम गुण" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" attr_login: "Username attribute" attr_mail: "Email attribute" - base_dn: "बेस डी.एन." - host: "होस्ट" + base_dn: "Base DN" + host: "Host" onthefly: "Automatic user creation" - port: "पोर्ट" + port: "Port" tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "Repository" @@ -555,7 +554,7 @@ hi: color: "रंग" user: admin: "व्यवस्थापक" - auth_source: "प्रमाणीकरण का तरीका" + ldap_auth_source: "LDAP connection" current_password: "Current password" force_password_change: "अगले लॉगऑन पर पासवर्ड बदलवाएं" language: "भाषा " @@ -676,7 +675,7 @@ hi: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "is the wrong length (should be %{count} characters)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -898,7 +897,7 @@ hi: description: "' पासवर्ड पुष्टिकरण ' नए पासवर्ड फ़ील्ड में इनपुट से मेल खाना चाहिए ।" status: invalid_on_create: "is not a valid status for new users." - auth_source: + ldap_auth_source: error_not_found: "not found" member: principal_blank: "कृपया कम से कम एक समूह या उपयोगकर्ता चुनें।" @@ -1434,6 +1433,7 @@ hi: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1448,6 +1448,8 @@ hi: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Configuration guide' get_in_touch: "You have questions? Get in touch with us." @@ -1492,6 +1494,13 @@ hi: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1560,9 +1569,9 @@ hi: label_attachment_plural: "फ़ाइलें" label_attribute: "गुण" label_attribute_plural: "गुण" - label_auth_source: "प्रमाणीकरण का तरीका" - label_auth_source_new: "प्रमाणीकरण का नया तरीका" - label_auth_source_plural: "प्रमाणीकरण का तरीके" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "प्रमाणीकरण" label_available_project_work_package_categories: 'Available work package categories' label_available_project_work_package_types: 'Available work package types' @@ -1728,7 +1737,7 @@ hi: label_hierarchy_leaf: "पदानुक्रम पत्ती" label_home: "मुखपृष्ठ" label_subject_or_id: "Subject or ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "में" label_in_less_than: "से भी कम समय में" @@ -2085,10 +2094,7 @@ hi: label_global_role: "Global Role" label_not_changeable: "(not changeable)" label_global: "Global" - auth_source: - using_abstract_auth_source: "Can't use an abstract authentication source." - ldap_error: "LDAP-Error: %{error_message}" - ldap_auth_failed: "Could not authenticate at the LDAP-Server." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Error executing the macro %{macro_name}" macro_unavailable: "Macro %{macro_name} cannot be displayed." macros: @@ -2277,7 +2283,7 @@ hi: present_access_key_value: "Your %{key_name} is: %{value}" notice_automatic_set_of_standard_type: "Set standard type automatically." notice_logged_out: "You have been logged out." - notice_wont_delete_auth_source: The authentication mode cannot be deleted as long as there are still users using it. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields. The project is invalid: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2764,12 +2770,16 @@ hi: text_work_packages_destroy_confirmation: "Are you sure you want to delete the selected work package(s)?" text_work_packages_ref_in_commit_messages: "Referencing and fixing work packages in commit messages" text_journal_added: "%{label} %{value} added" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} updated" text_journal_changed_with_diff: "%{label} changed (%{link})" text_journal_deleted: "%{label} deleted (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} deleted (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} set to %{value}" text_journal_set_with_diff: "%{label} set (%{link})" diff --git a/config/locales/crowdin/hr.yml b/config/locales/crowdin/hr.yml index c88fbf10f31..e69cce689cf 100644 --- a/config/locales/crowdin/hr.yml +++ b/config/locales/crowdin/hr.yml @@ -99,15 +99,14 @@ hr: edit: "Edit help text for %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: Trenutno ne postoji mehanizam provjere autentičnosti. - no_results_content_text: Dodaj novi mod provjere autentičnosti background_jobs: status: error_requeue: "Job experienced an error but is retrying. The error was: %{message}" cancelled_due_to: "Job was cancelled due to error: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | This LDAP form requires technical knowledge of your LDAP / Active Directory setup.
    @@ -450,13 +449,13 @@ hr: attribute_help_text: attribute_name: 'Atribut' help_text: 'Help text' - auth_source: - account: "Korisnički račun" - attr_firstname: "Atribut Ime" - attr_lastname: "Atribut Prezime" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" attr_login: "Username attribute" - attr_mail: "Email atribut" - base_dn: "Temeljni DN" + attr_mail: "Email attribute" + base_dn: "Base DN" host: "Host" onthefly: "Automatic user creation" port: "Port" @@ -558,7 +557,7 @@ hr: color: "Boja" user: admin: "Administrator" - auth_source: "Provjera autentičnosti" + ldap_auth_source: "LDAP connection" current_password: "Trenutna lozinka" force_password_change: "Promjena lozinke pri sljedećoj prijavi na sustav" language: "Jezik" @@ -679,7 +678,7 @@ hr: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "je krive duljine (mora biti %{count} znakova)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -902,8 +901,8 @@ hr: description: "' Potvrda lozinke' mora se podudarati s unosom unutar polja 'Nova lozinka'." status: invalid_on_create: "is not a valid status for new users." - auth_source: - error_not_found: "nije pronađeno" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "Molim vas odaberite najamnje jednog korisnika ili grupu." role_blank: "need to be assigned." @@ -1455,6 +1454,7 @@ hr: changes_retracted: "The changes were retracted." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1469,6 +1469,8 @@ hr: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Vodič za konfiguraciju' get_in_touch: "You have questions? Get in touch with us." @@ -1513,6 +1515,13 @@ hr: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1581,9 +1590,9 @@ hr: label_attachment_plural: "Datoteke" label_attribute: "Atribut" label_attribute_plural: "Atributi" - label_auth_source: "Provjera autentičnosti" - label_auth_source_new: "Novi način provjere autentičnosti" - label_auth_source_plural: "Provjera autentičnosti" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Autentifikacija" label_available_project_work_package_categories: 'Dostupne kategorije radnih paketa' label_available_project_work_package_types: 'Available work package types' @@ -1749,7 +1758,7 @@ hr: label_hierarchy_leaf: "Hierarchy leaf" label_home: "Početna" label_subject_or_id: "Subject or ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Legal notice" label_in: "u" label_in_less_than: "u manje od" @@ -2106,10 +2115,7 @@ hr: label_global_role: "Globalne role" label_not_changeable: "(ne promijenjiv)" label_global: "Globalne" - auth_source: - using_abstract_auth_source: "Ne možete se koristiti apstraktni izvor autentifikacije." - ldap_error: "LDAP-Greška: %{error_message}" - ldap_auth_failed: "Neuspjela autentifikacija na LDAP poslužitelju." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Došlo je do pogreške tijekom izvođenja makroa %{macro_name}" macro_unavailable: "Makro %{macro_name} ne možete biti prikazan." macros: @@ -2299,7 +2305,7 @@ hr: present_access_key_value: "Vaš %{key_name} je: %{value}" notice_automatic_set_of_standard_type: "Automatski postavite standardni tip." notice_logged_out: "Uspješno ste odjavljeni." - notice_wont_delete_auth_source: Oblik autentifikacije ne može biti izbrisan sve dok postoje korisnici koji ga još koriste. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields. The project is invalid: %{errors}" notice_attachment_migration_wiki_page: > This page was generated automatically during the update of OpenProject. It contains all attachments previously associated with the %{container_type} "%{container_name}". @@ -2787,12 +2793,16 @@ hr: text_work_packages_destroy_confirmation: "Are you sure you want to delete the selected work package(s)?" text_work_packages_ref_in_commit_messages: "Referencing and fixing work packages in commit messages" text_journal_added: "%{label}%{value} dodana" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} updated" text_journal_changed_with_diff: "%{label} promijenjen (%{link})" text_journal_deleted: "%{label} izbrisan (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} izbrisan (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} postavljen na %{value}" text_journal_set_with_diff: "%{label} postavljen (%{link})" diff --git a/config/locales/crowdin/hu.yml b/config/locales/crowdin/hu.yml index 8a2ed707aa6..89c437dda02 100644 --- a/config/locales/crowdin/hu.yml +++ b/config/locales/crowdin/hu.yml @@ -99,15 +99,14 @@ hu: edit: "Súgószöveg \"%{attribute_caption}\" szerkesztése" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: Jelenleg nincs autentikációs mód. - no_results_content_text: Új autentikációs mód létrehozása background_jobs: status: error_requeue: "A művelet hibát észlelt, de újra próbálkozik. A hiba a következő volt: %{message}\n" cancelled_due_to: "A műveletet hiba miatt törölték: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | Ehhez az LDAP űrlaphoz az Ön LDAP / Acitive Directory beállításainak ismerete szükséges.
    @@ -446,16 +445,16 @@ hu: attribute_help_text: attribute_name: 'Attribútum' help_text: 'Súgószöveg' - auth_source: - account: "Felhasználói fiók" - attr_firstname: "Keresztnév attribútum" - attr_lastname: "Vezetéknév attribútum" - attr_login: "Felhasználónév attribútum" - attr_mail: "Email attribútum" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" + attr_login: "Username attribute" + attr_mail: "Email attribute" base_dn: "Base DN" host: "Host" - onthefly: "Automatikus felhasználó készítés" - port: "Port szám" + onthefly: "Automatic user creation" + port: "Port" tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "Csomagtároló" @@ -554,7 +553,7 @@ hu: color: "Szín" user: admin: "Adminisztrátor" - auth_source: "Hitelesítési mód" + ldap_auth_source: "LDAP connection" current_password: "Jelenlegi jelszó" force_password_change: "Kötelező jelszóváltás a következő bejelentkezéskor" language: "Nyelv" @@ -675,7 +674,7 @@ hu: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "nem megfelelő hosszúságú (%{count} karakter szükséges)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -897,8 +896,8 @@ hu: description: "A \"'Jelszó megerősítés\"-nek meg kell egyeznie az \"Új jelszó\" mezővel." status: invalid_on_create: "ez nem egy érvényes állapot, az új felhasználó számára." - auth_source: - error_not_found: "nem található" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "Kérem, válasszon legalább egy felhasználót vagy csoportot." role_blank: "Hozzá kell rendelve lennie" @@ -1433,6 +1432,7 @@ hu: changes_retracted: "A módosítások visszavonásra kerültek." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1447,6 +1447,8 @@ hu: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Konfigurációs útmutató' get_in_touch: "Kérdése van? Lépjen kapcsolatba velünk" @@ -1491,6 +1493,13 @@ hu: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Értesítések küldése ehhez a művelethez\n" work_packages: @@ -1559,9 +1568,9 @@ hu: label_attachment_plural: "Fájlok" label_attribute: "Attribútum" label_attribute_plural: "Attribútumok" - label_auth_source: "Hitelesítési mód" - label_auth_source_new: "Új hitelesítési mód" - label_auth_source_plural: "Hitelesítési mód" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Hitelesítés" label_available_project_work_package_categories: 'Rendelkezésre álló munkacsomag kategóriák' label_available_project_work_package_types: 'Rendelkezésre álló munkacsomag típusok' @@ -1727,7 +1736,7 @@ hu: label_hierarchy_leaf: "Hierarchia szint" label_home: "Home" label_subject_or_id: "Tárgy, vagy azonosító" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Jogi nyilatkozat" label_in: "in" label_in_less_than: "kevesebb, mint" @@ -2084,10 +2093,7 @@ hu: label_global_role: "Globális szerep" label_not_changeable: "(nem módosítható)" label_global: "Globális" - auth_source: - using_abstract_auth_source: "Nem használható egy absztrakt hitelesítési forrás." - ldap_error: "LDAP-hiba: %{error_message}" - ldap_auth_failed: "A hitelesítés meghiúsult az LDAP-kiszolgálón." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Hiba a makro %{macro_name} végrehajtásában" macro_unavailable: "A %{macro_name} makró nem jeleníthető meg." macros: @@ -2275,7 +2281,7 @@ hu: present_access_key_value: "A Te %{key_name} is: %{value}" notice_automatic_set_of_standard_type: "Automatikusan beállítja alap típust ." notice_logged_out: "Kijelentkeztél." - notice_wont_delete_auth_source: Az autentikációs mód nem törölhető, amíg valamelyik felhasználó azt használja. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "Nem tudod firssíteni a projekt egyedi mezőit. A projekt érvénytelen: %{errors}" notice_attachment_migration_wiki_page: > Ez az oldal automatikusan jött létre az OpenProject frissítésekor. Tartalmazza az összes mellékletet az alábbiakhoz: %{container_type} %{container_name}. @@ -2762,12 +2768,16 @@ hu: text_work_packages_destroy_confirmation: "Biztosan törli a kijelölt feladatcsoportot?" text_work_packages_ref_in_commit_messages: "Viszonyítandó és elintézendő feladatcsoportok a véglegesítési üzenetekben" text_journal_added: "%{label} %{value} hozzáadva" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} megváltozott %{old} %{linebreak}-tól %{new} -ig" text_journal_changed_no_detail: "%{label} frissítve" text_journal_changed_with_diff: "%{label} megváltozott (%{link})" text_journal_deleted: "%{label} törölve (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} törölt (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} beállított %{value}" text_journal_set_with_diff: "%{label} beállított (%{link})" diff --git a/config/locales/crowdin/id.yml b/config/locales/crowdin/id.yml index ca95ec2086e..57b396040ae 100644 --- a/config/locales/crowdin/id.yml +++ b/config/locales/crowdin/id.yml @@ -99,15 +99,14 @@ id: edit: "Mengedit teks bantuan untuk %{attribute_caption}" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: Tidak ada mode otentikasi saat ini. - no_results_content_text: Buat mode otentikasi baru background_jobs: status: error_requeue: "Pekerjaan mengalami kesalahan tetapi mencoba lagi. Kesalahannya adalah: %{message}" cancelled_due_to: "Pekerjaan dibatalkan karena kesalahan: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | Formulir LDAP ini membutuhkan pengetahuan teknis LDAP anda / pengaturan Direktori Aktif.
    Silahkan kungjungi dokumentasi untuk instruksi lebih jelas. attribute_texts: @@ -441,15 +440,15 @@ id: attribute_help_text: attribute_name: 'Atribut' help_text: 'Teks bantuan' - auth_source: - account: "Akun" - attr_firstname: "Atribut nama depan" - attr_lastname: "Atribut nama belakang" - attr_login: "Atribut nama pengguna" - attr_mail: "Atribut email" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" + attr_login: "Username attribute" + attr_mail: "Email attribute" base_dn: "Base DN" host: "Host" - onthefly: "Otomatis membuat pengguna" + onthefly: "Automatic user creation" port: "Port" tls_certificate_string: "LDAP server SSL certificate" changeset: @@ -549,7 +548,7 @@ id: color: "Warna" user: admin: "Administrator" - auth_source: "Mode otentikasi" + ldap_auth_source: "LDAP connection" current_password: "Current password" force_password_change: "Ubah password saat pertama Login" language: "Bahasa" @@ -670,7 +669,7 @@ id: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "panjang tidak sesuai (harus %{count} karakter)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -891,8 +890,8 @@ id: description: "Isian konfirmasi password tidak sama dengan masukan password." status: invalid_on_create: "bukan status yang valid untuk user baru." - auth_source: - error_not_found: "tidak ditemukan" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "Silakan pilih setidaknya satu user atau grup." role_blank: "need to be assigned." @@ -1410,6 +1409,7 @@ id: changes_retracted: "Perubahan ditarik kembali." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1424,6 +1424,8 @@ id: dates: working: "%{date} sekarang berkerja" non_working: "%{date} sekarang tidak berkerja" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Petunjuk Konfigurasi' get_in_touch: "Anda punya pertanyaan? Hubungi kami." @@ -1468,6 +1470,13 @@ id: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Kirim pemberitahuan untuk tindakan ini" work_packages: @@ -1536,9 +1545,9 @@ id: label_attachment_plural: "File" label_attribute: "Atribut" label_attribute_plural: "Atribut" - label_auth_source: "Mode otentikasi" - label_auth_source_new: "Mode otentikasi baru" - label_auth_source_plural: "Mode otentikasi" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Otentikasi" label_available_project_work_package_categories: 'Available work package categories' label_available_project_work_package_types: 'Jenis paket pekerjaan yang tersedia' @@ -1704,7 +1713,7 @@ id: label_hierarchy_leaf: "Daun hierarki" label_home: "Home" label_subject_or_id: "Subyek atau ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Pernyataan hukum" label_in: "dalam" label_in_less_than: "kurang dari" @@ -2061,10 +2070,7 @@ id: label_global_role: "Peran global" label_not_changeable: "(tidak berubah)" label_global: "Global" - auth_source: - using_abstract_auth_source: "Can't use an abstract authentication source." - ldap_error: "LDAP-Error: %{error_message}" - ldap_auth_failed: "Authentikasi pada LDAP-Server gagal." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "ERROR eksekusi makro %{macro_name}" macro_unavailable: "Makro %{macro_name} tidak dapat ditampilkan." macros: @@ -2250,7 +2256,7 @@ id: present_access_key_value: "Your %{key_name} is: %{value}" notice_automatic_set_of_standard_type: "Set Tipe standar secara otomatis." notice_logged_out: "Anda telah logout." - notice_wont_delete_auth_source: The authentication mode cannot be deleted as long as there are still users using it. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields. The project is invalid: %{errors}" notice_attachment_migration_wiki_page: > Halaman ini dihasilkan secara otomatis selama pembaharuan OpenProject. Ini berisi semua lampiran yang terkait sebelumnya dengan %{container_type} "%{container_name}". @@ -2734,12 +2740,16 @@ id: text_work_packages_destroy_confirmation: "Yakin akan menghapus?" text_work_packages_ref_in_commit_messages: "Referencing & fixing Paket-Penugasan di pesan terkirim" text_journal_added: "%{label} %{value} ditambahkan" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label} telah diupdate" text_journal_changed_with_diff: "%{label} dirubah menjadi (%{link})" text_journal_deleted: "%{label} telah dihapus (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} telah dihapus menjadi (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} diset menjadi %{value}" text_journal_set_with_diff: "set %{label} (%{link})" diff --git a/config/locales/crowdin/it.yml b/config/locales/crowdin/it.yml index d98b5d04dba..e60135520e4 100644 --- a/config/locales/crowdin/it.yml +++ b/config/locales/crowdin/it.yml @@ -99,15 +99,14 @@ it: edit: "Modifica il testo guida per %{attribute_caption}" enterprise: description: 'Fornisci informazioni aggiuntive per gli attributi (inclusi i campi personalizzati) di macro-attività e progetti. I testi della guida vengono visualizzati quando gli utenti cliccano sul simbolo del punto interrogativo accanto ai campi di immissione nei progetti e nelle macro-attività.' - auth_sources: - index: - no_results_content_title: Al momento non vi è alcuna modalità di autenticazione. - no_results_content_text: Crea una nuova modalità di autenticazione background_jobs: status: error_requeue: "Si è verificato un errore col lavoro ma si sta riprovando. Errore: %{message}" cancelled_due_to: "Il lavoro è stato annullato a causa di un errore: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | Questo form LDAP richiede una conoscenza tecnica dell'attuale setup LDAP / Active Directory.
    Per favore visita la nostra documentazione per istruzioni dettagliate. @@ -446,17 +445,17 @@ it: attribute_help_text: attribute_name: 'Attributo' help_text: 'Testo guida' - auth_source: + ldap_auth_source: account: "Account" - attr_firstname: "Attributo nome" - attr_lastname: "Attributo cognome" - attr_login: "Attributo Nome Utente" - attr_mail: "Attributo email" - base_dn: "Nome Dominio (DN) base" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" + attr_login: "Username attribute" + attr_mail: "Email attribute" + base_dn: "Base DN" host: "Host" - onthefly: "Creazione automatica degli utenti" - port: "Porta" - tls_certificate_string: "Certificato SSL del server LDAP" + onthefly: "Automatic user creation" + port: "Port" + tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "Archivio" comment: @@ -554,7 +553,7 @@ it: color: "Colore" user: admin: "Amministratore" - auth_source: "Modalità di autenticazione" + ldap_auth_source: "LDAP connection" current_password: "Password corrente" force_password_change: "Richiedi la modifica della password al prossimo login" language: "Linguaggio" @@ -675,10 +674,10 @@ it: non fornisce un "Contesto sicuro". Usa HTTPS o un indirizzo di loopback, come localhost. wrong_length: "è della lunghezza sbagliata (dovrebbe essere %{count} caratteri)." models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: - invalid_certificate: "Il certificato SSL fornito non è valido: %{additional_message}" + invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" format: "%{message}" attachment: attributes: @@ -897,8 +896,8 @@ it: description: "'Conferma password' deve coincidere con il testo del campo 'Nuova password'." status: invalid_on_create: "non è uno stato valido per i nuovi utenti." - auth_source: - error_not_found: "non trovato" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "Si prega di scegliere almeno un utente o un gruppo." role_blank: "deve essere assegnato." @@ -1433,6 +1432,7 @@ it: changes_retracted: "Le modifiche sono state annullate." caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1447,6 +1447,8 @@ it: dates: working: "%{date} è ora lavorativo" non_working: "%{date} è ora non lavorativo" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: 'Guida di configurazione' get_in_touch: "Hai dei dubbi? Mettiti in contatto con noi." @@ -1491,6 +1493,13 @@ it: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Invia notifiche per questa azione" work_packages: @@ -1559,9 +1568,9 @@ it: label_attachment_plural: "File" label_attribute: "Attributo" label_attribute_plural: "Attributi" - label_auth_source: "Modalità di autenticazione" - label_auth_source_new: "Nuova modalità di autenticazione" - label_auth_source_plural: "Modalità di autenticazione" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "Autenticazione" label_available_project_work_package_categories: 'Categorie di macro-attività disponibili' label_available_project_work_package_types: 'Tipi di pacchetti di lavoro disponibili' @@ -1727,7 +1736,7 @@ it: label_hierarchy_leaf: "Foglia della gerarchia" label_home: "Radice (home)" label_subject_or_id: "Oggetto o ID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "Note legali" label_in: "in" label_in_less_than: "in meno di" @@ -2084,10 +2093,7 @@ it: label_global_role: "Ruolo globale" label_not_changeable: "(non modificabile)" label_global: "Globale" - auth_source: - using_abstract_auth_source: "Non è possibile utilizzare una fonte di autenticazione astratta." - ldap_error: "Errore LDAP: %{error_message}" - ldap_auth_failed: "Non è possibile autenticare attraverso il Server LDAP." + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "Errore nell'esecuzione della macro %{macro_name}" macro_unavailable: "Non può essere visualizzata la macro %{macro_name}." macros: @@ -2276,7 +2282,7 @@ it: present_access_key_value: "Il tuo %{key_name} è: %{value}" notice_automatic_set_of_standard_type: "Impostare automaticamente il tipo predefinito." notice_logged_out: "Ti sei appena disconnesso." - notice_wont_delete_auth_source: La modalità di autenticazione non può essere eliminata finché ci sono ancora utenti che la utilizzano. + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "Non è possibile aggiornare i campi personalizzati del progetto disponibili. Il progetto non è valido: %{errors}" notice_attachment_migration_wiki_page: > Questa pagina è stata generata automaticamente durante l'aggiornamento di OpenProject. Contiene tutti gli allegati precedentemente connessi con il %{container_type} "%{container_name}". @@ -2763,12 +2769,16 @@ it: text_work_packages_destroy_confirmation: "Sei sicuro di che voler eliminare le/le macro-attività selezionata/e?" text_work_packages_ref_in_commit_messages: "Macro-attivita che sono correlate e/o modificate nei messaggi di conferma" text_journal_added: "%{label} %{value} aggiunto" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} modificato da %{old} %{linebreak}a %{new}" text_journal_changed_no_detail: "%{label} aggiornata" text_journal_changed_with_diff: "%{label} cambiato (%{link})" text_journal_deleted: "%{label} cancellato (%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label} cancellato (%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label} impostato su %{value}" text_journal_set_with_diff: "insieme di %{label} (%{link})" diff --git a/config/locales/crowdin/ja.yml b/config/locales/crowdin/ja.yml index 7744e2ac338..08f50c744cf 100644 --- a/config/locales/crowdin/ja.yml +++ b/config/locales/crowdin/ja.yml @@ -99,15 +99,14 @@ ja: edit: "%{attribute_caption} のヘルプ テキストを編集" enterprise: description: 'Provide additional information for attributes (incl. custom fields) of work packages and projects. Help texts are displayed when users click on the question mark symbol next to input fields in projects and work packages.' - auth_sources: - index: - no_results_content_title: 現在認証モードはありません。 - no_results_content_text: 新しい認証モードを作成 background_jobs: status: error_requeue: "ジョブにエラーが発生しましたが、再試行中です。エラー: %{message}" cancelled_due_to: "エラーが発生したため、ジョブはキャンセルされました。エラー: %{message}" ldap_auth_sources: + ldap_error: "LDAP-Error: %{error_message}" + ldap_auth_failed: "Could not authenticate at the LDAP-Server." + back_to_index: 'Click here to go back to the list of connection.' technical_warning_html: | この LDAP フォームには、LDAP / Active Directory の設定に関する技術的な知識が必要です。
    @@ -444,16 +443,16 @@ ja: attribute_help_text: attribute_name: '属性' help_text: 'ヘルプ テキスト' - auth_source: - account: "アカウント" - attr_firstname: "名前" - attr_lastname: "苗字" - attr_login: "ユーザー名属性" - attr_mail: "メール" - base_dn: "ベースDN" - host: "ホスト" - onthefly: "ユーザーの自動作成" - port: "ポート番号" + ldap_auth_source: + account: "Account" + attr_firstname: "Firstname attribute" + attr_lastname: "Lastname attribute" + attr_login: "Username attribute" + attr_mail: "Email attribute" + base_dn: "Base DN" + host: "Host" + onthefly: "Automatic user creation" + port: "Port" tls_certificate_string: "LDAP server SSL certificate" changeset: repository: "リポジトリ" @@ -552,7 +551,7 @@ ja: color: "色" user: admin: "管理者" - auth_source: "認証モード" + ldap_auth_source: "LDAP connection" current_password: "現在のパスワード" force_password_change: "次回ログイン時にパスワード変更を強制する" language: "言語" @@ -673,7 +672,7 @@ ja: is not providing a "Secure Context". Either use HTTPS or a loopback address, such as localhost. wrong_length: "は%{count}文字を入力してください。" models: - auth_source: + ldap_auth_source: attributes: tls_certificate_string: invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}" @@ -894,8 +893,8 @@ ja: description: "「パスワードの確認」は、「新しいパスワード」フィールドの入力と一致する必要があります。" status: invalid_on_create: "新規ユーザーの有効なステータスではありません。" - auth_source: - error_not_found: "存在しません" + ldap_auth_source: + error_not_found: "not found" member: principal_blank: "一つ以上のユーザまたはグループを選択してください。" role_blank: "を割り当てる必要があります。" @@ -1413,6 +1412,7 @@ ja: changes_retracted: "変更は取り下げられました。" caused_changes: dates_changed: "Dates changed" + system_update: "OpenProject update" cause_descriptions: work_package_predecessor_changed_times: by changes to predecessor %{link} work_package_parent_changed_times: by changes to parent %{link} @@ -1427,6 +1427,8 @@ ja: dates: working: "%{date} is now working" non_working: "%{date} is now non-working" + system_update: + file_links_journal: 'added File links to Activities' links: configuration_guide: '設定ガイド' get_in_touch: "ご質問がありますか?ご連絡ください。" @@ -1471,6 +1473,13 @@ ja: text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader." static_token_name: "RSS token" disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature." + storages: + title: "File Storages" + text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage." + empty_text_hint: "There is no storage access linked to your account." + revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}" + removed: "File Storage token successfully removed" + failed: "An error occurred and the token couldn't be removed. Please try again later." notifications: send_notifications: "Send notifications for this action" work_packages: @@ -1539,9 +1548,9 @@ ja: label_attachment_plural: "ファイルを添付する" label_attribute: "属性" label_attribute_plural: "属性" - label_auth_source: "認証モード" - label_auth_source_new: "新しい認証方式" - label_auth_source_plural: "認証方式" + label_ldap_auth_source_new: "New LDAP connection" + label_ldap_auth_source: "LDAP connection" + label_ldap_auth_source_plural: "LDAP connections" label_authentication: "認証" label_available_project_work_package_categories: '有効なワークパッケージのカテゴリ' label_available_project_work_package_types: '利用可能なワークパッケージのタイプ' @@ -1707,7 +1716,7 @@ ja: label_hierarchy_leaf: "階層枚" label_home: "ホーム" label_subject_or_id: "タイトルまたはID" - label_icalendar: "iCalendar" + label_calendar_subscriptions: "Calendar subscriptions" label_impressum: "法的情報" label_in: "今日から○日後" label_in_less_than: "今日から○日後以前" @@ -2064,10 +2073,7 @@ ja: label_global_role: "グローバルロール" label_not_changeable: "(変更不可)" label_global: "全般" - auth_source: - using_abstract_auth_source: "概要認証元を使用できません。" - ldap_error: "LDAPエラー: %{error_message}" - ldap_auth_failed: "LDAPサーバーで認証できませんでした。" + label_seeded_from_env_warning: This record has been created through a setting / environment variable. It is not editable through UI. macro_execution_error: "マクロ%{macro_name}を実行中にエラーが発生しました。" macro_unavailable: "マクロ%{macro_name}を表示できません。" macros: @@ -2255,7 +2261,7 @@ ja: present_access_key_value: "%{key_name} は: %{value}" notice_automatic_set_of_standard_type: "自動的に標準型を設定します。" notice_logged_out: "ログアウトしました。" - notice_wont_delete_auth_source: 認証モードは、まだ使用しているユーザが残っている限り、削除できません。 + notice_wont_delete_auth_source: The LDAP connection cannot be deleted as long as there are still users using it. notice_project_cannot_update_custom_fields: "プロジェクトの有効なカスタムフィールドを更新できません。プロジェクトは無効です: %{errors}" notice_attachment_migration_wiki_page: > このページは、OpenProject の更新時に自動的に生成されました。%{container_type} "%{container_name}" と関連付けられていたすべての添付ファイルが含まれています。 @@ -2741,12 +2747,16 @@ ja: text_work_packages_destroy_confirmation: "選択したワークパッケージを削除してもよろしいですか?" text_work_packages_ref_in_commit_messages: "コミットメッセージ内でのワークパッケージの参照 / 修正" text_journal_added: "%{label}で%{value}を追加" + text_journal_attachment_added: "%{label} %{value} added as attachment" + text_journal_attachment_deleted: "%{label} %{old} removed as attachment" text_journal_changed_plain: "%{label} changed from %{old} %{linebreak}to %{new}" text_journal_changed_no_detail: "%{label}を更新" text_journal_changed_with_diff: "%{label}を変更(%{link})" text_journal_deleted: "%{label}を削除(%{old})" text_journal_deleted_subproject: "%{label} %{old}" text_journal_deleted_with_diff: "%{label}を削除(%{link})" + text_journal_file_link_added: "%{label} link to %{value} (%{storage}) added" + text_journal_file_link_deleted: "%{label} link to %{old} (%{storage}) removed" text_journal_of: "%{label} %{value}" text_journal_set_to: "%{label}を%{value}に設定" text_journal_set_with_diff: "%{label}を設定(%{link})" diff --git a/config/locales/crowdin/js-af.yml b/config/locales/crowdin/js-af.yml index 177b691e408..a29ea78331c 100644 --- a/config/locales/crowdin/js-af.yml +++ b/config/locales/crowdin/js-af.yml @@ -342,21 +342,22 @@ af: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activate" label_assignee: 'Gedelegeerde' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ af: save_as: "Save as" export: "Export" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Skrap" filter: "Filtreer" @@ -1273,6 +1274,7 @@ af: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-ar.yml b/config/locales/crowdin/js-ar.yml index de532ef12a3..50ba65dd320 100644 --- a/config/locales/crowdin/js-ar.yml +++ b/config/locales/crowdin/js-ar.yml @@ -342,21 +342,22 @@ ar: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "تفعيل" label_assignee: 'المُسند إليه' label_add_column_after: "Add column after" @@ -1084,7 +1085,7 @@ ar: save_as: "احفظ بشكل" export: "تصدير" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "احذف" filter: "فلترة" @@ -1283,6 +1284,7 @@ ar: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-az.yml b/config/locales/crowdin/js-az.yml index adbbfc256e5..d9879262b63 100644 --- a/config/locales/crowdin/js-az.yml +++ b/config/locales/crowdin/js-az.yml @@ -342,21 +342,22 @@ az: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activate" label_assignee: 'Assignee' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ az: save_as: "Fərqli saxla" export: "İxrac et" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Sil" filter: "Filtr" @@ -1273,6 +1274,7 @@ az: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-be.yml b/config/locales/crowdin/js-be.yml index e1fc7aa416d..5357cd1b99f 100644 --- a/config/locales/crowdin/js-be.yml +++ b/config/locales/crowdin/js-be.yml @@ -342,21 +342,22 @@ be: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activate" label_assignee: 'Прызначаная асоба' label_add_column_after: "Add column after" @@ -1082,7 +1083,7 @@ be: save_as: "Save as" export: "Export" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Выдаліць" filter: "Фільтр" @@ -1279,6 +1280,7 @@ be: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-bg.yml b/config/locales/crowdin/js-bg.yml index 29883a5fad1..9232d68b891 100644 --- a/config/locales/crowdin/js-bg.yml +++ b/config/locales/crowdin/js-bg.yml @@ -342,21 +342,22 @@ bg: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Активиране" label_assignee: 'Изпълнител' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ bg: save_as: "Запиши като" export: "Експортиране" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Изтрий" filter: "Филтър" @@ -1273,6 +1274,7 @@ bg: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-ca.yml b/config/locales/crowdin/js-ca.yml index e5c3a3a5934..348ce4272f8 100644 --- a/config/locales/crowdin/js-ca.yml +++ b/config/locales/crowdin/js-ca.yml @@ -342,21 +342,22 @@ ca: learn_about: "Més informació sobre les noves funcions" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - Aquesta versió inclou noves funcionalitats i millores:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscripció via iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "S'ha produït un error en carregar les dades." - description: "Pots utilitzar l'URL d'iCalendar per importar o subscriure't a aquest calendari en un client extern i veure les informacions dels paquets de treball en temps real des d'allà." - warning: "Si us plau, no comparteixis aquesta URL amb usuaris externs. Qualsevol persona amb aquest enllaç tindrà accés als detalls dels paquets de treball sense necessitat de tenir un compte o contrasenya." - token_name_label: "Nom del token" - token_name_placeholder: "Introdueix un nom per aquest token, ex. \"La meva aplicació de calendari\"" - token_name_description_text: "Aquest nom s'utilitzarà per distingir-lo d'altres tokens en la llista de tokens d'accés. Només es pot utilitzar un cop per calendari." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copia l'URL" - ical_generation_error_text: "S'ha produït un error en generar l'URL del iCal." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activar" label_assignee: 'Assignat a' label_add_column_after: "Afegir una columna després" @@ -1080,7 +1081,7 @@ ca: save_as: "Desa com a" export: "Exporta" visibility_settings: "Configuració de visibilitat" - share_calendar: "Subscripció via iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Reanomena la vista" delete: "Elimina" filter: "Filtre" @@ -1273,6 +1274,7 @@ ca: between_two_specific_dates: 'entre dues dates específiques' legends: changes_since: 'Canvis des de' + changes_between: 'Changes between' now_meets_filter_criteria: 'Ara compleix els criteris del filtre' no_longer_meets_filter_criteria: 'Ja no compleix els criteris del filtre' maintained_with_changes: 'Mantingut amb canvis' diff --git a/config/locales/crowdin/js-ckb-IR.yml b/config/locales/crowdin/js-ckb-IR.yml index 94b2f74c797..7ab05972192 100644 --- a/config/locales/crowdin/js-ckb-IR.yml +++ b/config/locales/crowdin/js-ckb-IR.yml @@ -342,21 +342,22 @@ ckb-IR: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activate" label_assignee: 'Assignee' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ ckb-IR: save_as: "Save as" export: "Export" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Delete" filter: "Filter" @@ -1273,6 +1274,7 @@ ckb-IR: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-cs.yml b/config/locales/crowdin/js-cs.yml index 4eb84a7693e..fe3f784a96e 100644 --- a/config/locales/crowdin/js-cs.yml +++ b/config/locales/crowdin/js-cs.yml @@ -297,17 +297,17 @@ cs: two: "Druhé kritérium třídění" three: "Třetí kritérium třídění" gantt_chart: - label: 'Gantt chart' + label: 'Ganttův diagram' quarter_label: 'Q%{quarter_number}' labels: title: 'Label configuration' bar: 'Bar labels' - left: 'Left' - right: 'Right' - farRight: 'Far right' + left: 'Vlevo' + right: 'Vpravo' + farRight: 'Dále vpravo' description: > Select the attributes you want to be shown in the respective positions of the Gantt chart at all times. Note that when hovering over an element, its date labels will be shown instead of these attributes. - button_activate: 'Show Gantt chart' + button_activate: 'Zobrazit Ganttův diagram' button_deactivate: 'Skrýt Gantt' filter: noneSelection: "(žádný)" @@ -342,21 +342,22 @@ cs: learn_about: "Další informace o nových funkcích" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-0-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - Vydání obsahuje různé nové funkce a vylepšení:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Přihlásit se k odběru iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "Při načítání dat došlo k chybě." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Název tokenu" - token_name_placeholder: "Zadejte název pro tento token, např. \"Moje aplikace v kalendáři\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Kde to použijete?" + token_name_placeholder: "Zadejte jméno, např. \"Telefon\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Kopírovat URL" - ical_generation_error_text: "Došlo k chybě při generování adresy URL pro iCal." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Aktivovat" label_assignee: 'Řešitel' label_add_column_after: "Přidat sloupec za" @@ -622,7 +623,7 @@ cs: many: 'a %{count} dalšich' other: 'a %{count} dalšich' no_results: - at_all: 'New notifications will appear here when there is activity that concerns you.' + at_all: 'Nová oznámení se zobrazí zde, když se objeví aktivita, která se vás týká' with_current_filter: 'V současné době nejsou v tomto zobrazení žádná oznámení' mark_all_read: 'Označit vše jako přečtené' mark_as_read: 'Označit jako přečteno' @@ -1082,7 +1083,7 @@ cs: save_as: "Uložit jako" export: "Export" visibility_settings: "Nastavení viditelnosti" - share_calendar: "Přihlásit se k odběru iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Přejmenovat zobrazení" delete: "Odstranit" filter: "Filtr" @@ -1279,6 +1280,7 @@ cs: between_two_specific_dates: 'mezi dvěma konkrétními daty' legends: changes_since: 'Změny od' + changes_between: 'Changes between' now_meets_filter_criteria: 'Nyní splňuje kritéria filtru' no_longer_meets_filter_criteria: 'Již nesplňuje kritéria filtru' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-da.yml b/config/locales/crowdin/js-da.yml index faf9e3f1dfd..628cf79a935 100644 --- a/config/locales/crowdin/js-da.yml +++ b/config/locales/crowdin/js-da.yml @@ -341,21 +341,22 @@ da: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Aktivér" label_assignee: 'Tildelt' label_add_column_after: "Add column after" @@ -1079,7 +1080,7 @@ da: save_as: "Gem som" export: "Eksportér" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Slet" filter: "Filter" @@ -1272,6 +1273,7 @@ da: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-de.yml b/config/locales/crowdin/js-de.yml index bb5907b1067..cf89dbb340d 100644 --- a/config/locales/crowdin/js-de.yml +++ b/config/locales/crowdin/js-de.yml @@ -296,33 +296,33 @@ de: two: "Zweites Sortierkriterium" three: "Drittes Sortierkriterium" gantt_chart: - label: 'Gantt chart' + label: 'Gantt-Diagramm' quarter_label: 'Q%{quarter_number}' labels: - title: 'Label configuration' - bar: 'Bar labels' - left: 'Left' - right: 'Right' - farRight: 'Far right' + title: 'Konfiguration von Beschriftungen' + bar: 'Balkenbeschriftungen' + left: 'Links' + right: 'Rechts' + farRight: 'Rechts außen' description: > - Select the attributes you want to be shown in the respective positions of the Gantt chart at all times. Note that when hovering over an element, its date labels will be shown instead of these attributes. - button_activate: 'Show Gantt chart' - button_deactivate: 'Hide Gantt chart' + Wählen Sie die Attribute, die an den jeweiligen Positionen des Gantt-Diagramms zu jeder Zeit angezeigt werden sollen. Bedenken Sie, dass beim Überfahren eines Elements die Datumswerte angezeigt werden anstatt dieser Attribute. + button_activate: 'Gantt-Diagramm zeigen' + button_deactivate: 'Gantt-Diagramm ausblenden' filter: - noneSelection: "(none)" + noneSelection: "(keine)" selection_mode: - notification: 'Click on any highlighted work package to create the relation. Press escape to cancel.' + notification: 'Klicken Sie auf ein hervorgehobenes Arbeitspaket, um eine Relation zu diesem zu erstellen. Drücken Sie Escape, um den Modus abzubrechen.' zoom: - in: "Zoom in" - out: "Zoom out" - auto: "Auto zoom" - days: "Days" - weeks: "Weeks" - months: "Months" - quarters: "Quarters" - years: "Years" + in: "Mehr Details" + out: "Weniger Details" + auto: "Auto-Zoom" + days: "Tage" + weeks: "Wochen" + months: "Monate" + quarters: "Quartale" + years: "Jahre" description: > - Select the initial zoom level that should be shown when autozoom is not available. + Wählen Sie die Zoomstufe für das Gantt-Diagramm aus, wenn kein Autozoom aktiviert ist. general_text_no: "nein" general_text_yes: "ja" general_text_No: "Nein" @@ -341,21 +341,22 @@ de: learn_about: "Erfahren Sie mehr über die neuen Funktionen" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release new_features_html: > - Die Version enthält verschiedene neue Funktionen und Verbesserungen:
    + Das Release enthält verschiedene neue Funktionen und Verbesserungen:
    ical_sharing_modal: - title: "iCalendar abonnieren" + title: "Kalender abonnieren" inital_setup_error_message: "Beim Abrufen der Daten ist ein Fehler aufgetreten." - description: "Sie können die iCalendar-URL verwenden, um diesen Kalender in einen externen Client zu importieren oder zu abonnieren und von dort aus aktuelle Arbeitspaketinformationen einsehen." - warning: "Bitte geben Sie diese URL nicht an externe Nutzer weiter. Jeder, der über diesen Link verfügt, kann die Details des Arbeitspakets ohne Konto oder Passwort einsehen." - token_name_label: "Tokenname" - token_name_placeholder: "Geben Sie einen Namen für dieses Token ein, z. B. „Meine Kalenderanwendung“." - token_name_description_text: "Dieser Name wird verwendet, um ihn von anderen Token in der Zugangs-Token-Liste zu unterscheiden. Er kann nur einmal pro Kalender verwendet werden." + description: "Sie können die URL (iCalendar) verwenden, um diesen Kalender in einem externen Client zu abonnieren und aktuelle Arbeitspaket-Informationen von dort aus anzuzeigen." + warning: "Bitte teilen Sie diese URL nicht mit anderen Benutzern. Jeder mit diesem Link kann die Arbeitspaket-Details ohne Konto oder Passwort anzeigen." + token_name_label: "Wo werden Sie diesen Kalender nutzen?" + token_name_placeholder: "Geben sie einen Namen ein (z.B. \"Telefon\")" + token_name_description_text: "Wenn Sie diesen Kalender auf mehreren Geräten abonnieren, wird Ihnen dieser Name helfen, zwischen diesen in der Liste der Zugriffstoken zu unterscheiden." copy_url_label: "URL kopieren" - ical_generation_error_text: "Beim Generieren der iCal-URL ist ein Fehler aufgetreten." + ical_generation_error_text: "Beim Generieren der Kalender-URL ist ein Fehler aufgetreten." + success_message: "Die URL \"%{name}\" wurde erfolgreich in Ihre Zwischenablage kopiert. Fügen Sie sie in Ihren Kalender-Client ein, um das Abonnement abzuschließen." label_activate: "Aktiviere" label_assignee: 'Zugewiesen an' label_add_column_after: "Spalte danach hinzufügen" @@ -914,10 +915,10 @@ de: addition_label: 'Innerhalb des Vergleichszeitraums zur Ansicht hinzugefügt' removal_label: 'Innerhalb des Vergleichszeitraums aus der Ansicht entfernt' modification_label: 'Innerhalb des Vergleichszeitraums geändert' - column_incompatible: 'This column does not show changes in Baseline mode.' + column_incompatible: 'Diese Spalte zeigt keine Änderungen im Basislinien-Modus.' filters: title: 'Arbeitspakete filtern' - baseline_incompatible: 'This filter attribute is not taken into consideration in Baseline mode.' + baseline_incompatible: 'Dieses Filterattribut wird im Basislininen-Modus nicht berücksichtigt.' baseline_warning: 'Der Baseline-Vergleich ist aktiviert, aber einige Ihrer aktiven Filter sind nicht im Vergleich enthalten.' inline_create: title: 'Klicken Sie hier, um ein neues Arbeitspaket zu dieser Liste hinzufügen' @@ -1079,7 +1080,7 @@ de: save_as: "Speichern unter" export: "Exportieren" visibility_settings: "Sichtbarkeits-Einstellungen" - share_calendar: "iCalendar abonnieren" + share_calendar: "Kalender abonnieren" page_settings: "Ansicht umbenennen" delete: "Löschen" filter: "Filter" @@ -1272,6 +1273,7 @@ de: between_two_specific_dates: 'zwischen zwei bestimmten Daten' legends: changes_since: 'Änderungen seit' + changes_between: 'Änderungen zwischen' now_meets_filter_criteria: 'Erfüllt nun die Filterkriterien' no_longer_meets_filter_criteria: 'Erfüllt Filterkriterien nicht mehr' maintained_with_changes: 'Änderungen erhalten' diff --git a/config/locales/crowdin/js-el.yml b/config/locales/crowdin/js-el.yml index c15a48d2286..89150e982e3 100644 --- a/config/locales/crowdin/js-el.yml +++ b/config/locales/crowdin/js-el.yml @@ -341,21 +341,22 @@ el: learn_about: "Μάθετε περισσότερα για τις νέες λειτουργίες" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Ενεργοποίηση" label_assignee: 'Ανάθεση σε' label_add_column_after: "Προσθήκη στήλης μετά" @@ -1079,7 +1080,7 @@ el: save_as: "Αποθήκευση ως" export: "Εξαγωγή" visibility_settings: "Ρυθμίσεις ορατότητας" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Μετονομασία προβολής" delete: "Διαγραφή" filter: "Φίλτρο" @@ -1272,6 +1273,7 @@ el: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-eo.yml b/config/locales/crowdin/js-eo.yml index 36350edc776..43fc54d0047 100644 --- a/config/locales/crowdin/js-eo.yml +++ b/config/locales/crowdin/js-eo.yml @@ -342,21 +342,22 @@ eo: learn_about: "Lerni pli pri la novaj plibonigoj" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activate" label_assignee: 'Asignita al' label_add_column_after: "Aldoni kolumnon dektre" @@ -1080,7 +1081,7 @@ eo: save_as: "Konservi kiel" export: "Eksporti" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Renomigu vidon" delete: "Forigi" filter: "Filtrilo" @@ -1273,6 +1274,7 @@ eo: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-es.yml b/config/locales/crowdin/js-es.yml index 61a9aaa1852..5de18564115 100644 --- a/config/locales/crowdin/js-es.yml +++ b/config/locales/crowdin/js-es.yml @@ -342,21 +342,22 @@ es: learn_about: "Más información sobre las nuevas funciones" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - La versión contiene varias nuevas funciones y mejoras:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activar" label_assignee: 'Asignado a' label_add_column_after: "Añadir columna a la derecha" @@ -1080,7 +1081,7 @@ es: save_as: "Guardar como" export: "Exportar" visibility_settings: "Configuración de visibilidad" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Renombrar vista" delete: "Borrar" filter: "Filtro" @@ -1273,6 +1274,7 @@ es: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-et.yml b/config/locales/crowdin/js-et.yml index 25c3842ee13..7cd0851cb33 100644 --- a/config/locales/crowdin/js-et.yml +++ b/config/locales/crowdin/js-et.yml @@ -342,21 +342,22 @@ et: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Aktiveerimine" label_assignee: 'Määratud tegija' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ et: save_as: "Salvesta kui" export: "Ekspordi" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Kustuta" filter: "Filter" @@ -1273,6 +1274,7 @@ et: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-eu.yml b/config/locales/crowdin/js-eu.yml index bcfe997b3e3..3a9cf469208 100644 --- a/config/locales/crowdin/js-eu.yml +++ b/config/locales/crowdin/js-eu.yml @@ -342,21 +342,22 @@ eu: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activate" label_assignee: 'Assignee' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ eu: save_as: "Save as" export: "Export" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Delete" filter: "Filter" @@ -1273,6 +1274,7 @@ eu: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-fa.yml b/config/locales/crowdin/js-fa.yml index ddb38aa1b98..32aa383f38d 100644 --- a/config/locales/crowdin/js-fa.yml +++ b/config/locales/crowdin/js-fa.yml @@ -342,21 +342,22 @@ fa: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activate" label_assignee: 'Assignee' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ fa: save_as: "ذخیره به عنوان" export: "خروجی" visibility_settings: "تنظیمات دیداری" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "حذف" filter: "Filter" @@ -1273,6 +1274,7 @@ fa: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-fi.yml b/config/locales/crowdin/js-fi.yml index 3e30e5ee109..776b0a57a0d 100644 --- a/config/locales/crowdin/js-fi.yml +++ b/config/locales/crowdin/js-fi.yml @@ -342,21 +342,22 @@ fi: learn_about: "Lisätietoja uusista ominaisuuksista" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Aktivoi" label_assignee: 'Työn suorittaja' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ fi: save_as: "Tallenna nimellä" export: "Vie" visibility_settings: "Näkyvyysasetukset" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Nimeä näkymä" delete: "Poista" filter: "Suodata" @@ -1273,6 +1274,7 @@ fi: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-fil.yml b/config/locales/crowdin/js-fil.yml index 32f420e54e4..7c096a4c0dc 100644 --- a/config/locales/crowdin/js-fil.yml +++ b/config/locales/crowdin/js-fil.yml @@ -342,21 +342,22 @@ fil: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Aktibo" label_assignee: 'Naitalaga' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ fil: save_as: "I-save bilang" export: "I-export" visibility_settings: "Ang mga setting ng katanyagan" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Burahin" filter: "Salain" @@ -1273,6 +1274,7 @@ fil: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-fr.yml b/config/locales/crowdin/js-fr.yml index 705f1730830..56baff7092b 100644 --- a/config/locales/crowdin/js-fr.yml +++ b/config/locales/crowdin/js-fr.yml @@ -342,21 +342,22 @@ fr: learn_about: "En savoir plus sur les nouvelles fonctionnalités" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - La version contient diverses nouvelles fonctionnalités et améliorations :
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copier l'URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activer" label_assignee: 'Responsable' label_add_column_after: "Ajouter une colonne après" @@ -1080,7 +1081,7 @@ fr: save_as: "Enregistrer sous" export: "Exporter" visibility_settings: "Paramètres de visibilité" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Renommer la vue" delete: "Supprimer" filter: "Filtrer" @@ -1273,6 +1274,7 @@ fr: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-he.yml b/config/locales/crowdin/js-he.yml index e9eb8276149..07a2e3fae09 100644 --- a/config/locales/crowdin/js-he.yml +++ b/config/locales/crowdin/js-he.yml @@ -342,21 +342,22 @@ he: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "הפעל" label_assignee: 'משויך אל' label_add_column_after: "Add column after" @@ -1082,7 +1083,7 @@ he: save_as: "שמירה בשם" export: "ייצוא" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "מחק" filter: "סינון" @@ -1279,6 +1280,7 @@ he: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-hi.yml b/config/locales/crowdin/js-hi.yml index e97bc0d3faf..1fe2c29125b 100644 --- a/config/locales/crowdin/js-hi.yml +++ b/config/locales/crowdin/js-hi.yml @@ -342,21 +342,22 @@ hi: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activate" label_assignee: 'अनुदिष्ट' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ hi: save_as: "के रूप में सहेजें..." export: "निर्यात" visibility_settings: "दृश्यता settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "मिटाएँ" filter: "Filter" @@ -1273,6 +1274,7 @@ hi: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-hr.yml b/config/locales/crowdin/js-hr.yml index 74ea81e006e..ba3948f60bf 100644 --- a/config/locales/crowdin/js-hr.yml +++ b/config/locales/crowdin/js-hr.yml @@ -342,21 +342,22 @@ hr: learn_about: "Saznaj više o novim značajkama" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Aktiviraj" label_assignee: 'Opunomoćeno' label_add_column_after: "Add column after" @@ -1081,7 +1082,7 @@ hr: save_as: "Spremiti kao" export: "Izvezi" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Obriši" filter: "Filter" @@ -1276,6 +1277,7 @@ hr: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-hu.yml b/config/locales/crowdin/js-hu.yml index a1a3bbd261e..41d38aa1d9d 100644 --- a/config/locales/crowdin/js-hu.yml +++ b/config/locales/crowdin/js-hu.yml @@ -342,21 +342,22 @@ hu: learn_about: "Tudjon meg többet az új funkciókról" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Aktivál" label_assignee: 'Megbízott' label_add_column_after: "Oszlop hozzáadása utána" @@ -1080,7 +1081,7 @@ hu: save_as: "Mentés másként" export: "Exportálás" visibility_settings: "Láthatósági beállítások" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Nézet átnevezése" delete: "Törlés" filter: "Szűrő" @@ -1273,6 +1274,7 @@ hu: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-id.yml b/config/locales/crowdin/js-id.yml index 8cb0ebbf262..754757fdfca 100644 --- a/config/locales/crowdin/js-id.yml +++ b/config/locales/crowdin/js-id.yml @@ -342,21 +342,22 @@ id: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Aktifkan" label_assignee: 'Pelimpahan' label_add_column_after: "Add column after" @@ -1079,7 +1080,7 @@ id: save_as: "Simpan sebagai" export: "Ekspor" visibility_settings: "Seting penampakan" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Hapus" filter: "Filter" @@ -1270,6 +1271,7 @@ id: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-it.yml b/config/locales/crowdin/js-it.yml index 5c1a342febc..03250ac57c8 100644 --- a/config/locales/crowdin/js-it.yml +++ b/config/locales/crowdin/js-it.yml @@ -342,21 +342,22 @@ it: learn_about: "Scopri di più sulle nuove funzionalità" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - La versione contiene varie nuove funzionalità e miglioramenti:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Iscriviti a iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "Si è verificato un errore recuperando i dati." - description: "Puoi utilizzare l'URL di iCalendar per importare o iscriverti a questo calendario su un client esterno e per visualizzare le informazioni sul pacchetto di lavoro aggiornate da lì." - warning: "Sei pregato di non condividerlo con utenti esterni. Chiunque abbia questo link potrà visualizzare i dettagli del pacchetto di lavoro senza un profilo o una password." - token_name_label: "Nome del token" - token_name_placeholder: "Digita un nome per questo token, es. \"Applicazione del mio calendario\"" - token_name_description_text: "Questo nome sarà utilizzato per distinguerlo da altri token nell'elenco di token d'accesso. Può essere utilizzato soltanto una volta per calendario." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copia URL" - ical_generation_error_text: "Si è verificato un errore generando l'URL di iCal." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Attiva" label_assignee: 'Assegnatario' label_add_column_after: "Aggiungi colonna dopo" @@ -1080,7 +1081,7 @@ it: save_as: "Salva come" export: "Esporta" visibility_settings: "Impostazioni di visibilità" - share_calendar: "Iscriviti a iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rinomina vista" delete: "Cancella" filter: "Filtro" @@ -1273,6 +1274,7 @@ it: between_two_specific_dates: 'tra due date specifiche' legends: changes_since: 'Modifiche dal' + changes_between: 'Changes between' now_meets_filter_criteria: 'Ora soddisfa i criteri del filtro' no_longer_meets_filter_criteria: 'Non soddisfa più i criteri del filtro' maintained_with_changes: 'Mantenuto con le modifiche' diff --git a/config/locales/crowdin/js-ja.yml b/config/locales/crowdin/js-ja.yml index 7ae02363329..42b6958a987 100644 --- a/config/locales/crowdin/js-ja.yml +++ b/config/locales/crowdin/js-ja.yml @@ -343,21 +343,22 @@ ja: learn_about: "新機能の詳細はこちら" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "URLをコピー" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "有効にする" label_assignee: '担当者' label_add_column_after: "後に列を追加" @@ -1080,7 +1081,7 @@ ja: save_as: "名前をつけて保存" export: "外部出力" visibility_settings: "表示の設定" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "ビューの名前を変更..." delete: "削除" filter: "フィルタ" @@ -1271,6 +1272,7 @@ ja: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-ka.yml b/config/locales/crowdin/js-ka.yml index e01765e1586..f3fdc1731fc 100644 --- a/config/locales/crowdin/js-ka.yml +++ b/config/locales/crowdin/js-ka.yml @@ -342,21 +342,22 @@ ka: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activate" label_assignee: 'Assignee' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ ka: save_as: "Save as" export: "Export" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Delete" filter: "Filter" @@ -1273,6 +1274,7 @@ ka: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-ko.yml b/config/locales/crowdin/js-ko.yml index 93354dc6dca..d4f76900ce1 100644 --- a/config/locales/crowdin/js-ko.yml +++ b/config/locales/crowdin/js-ko.yml @@ -342,21 +342,22 @@ ko: learn_about: "새로운 기능에 대해 자세히 알아보기" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - 이 릴리스에는 다음과 같은 다양한 새로운 기능과 개선 사항이 포함되어 있습니다.
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "활성화" label_assignee: '담당자' label_add_column_after: "다음 뒤에 열 추가" @@ -1079,7 +1080,7 @@ ko: save_as: "다른 이름으로 저장" export: "내보내기" visibility_settings: "표시 여부 설정" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "보기 이름 바꾸기" delete: "삭제" filter: "필터" @@ -1270,6 +1271,7 @@ ko: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-lt.yml b/config/locales/crowdin/js-lt.yml index dfa01939ed0..39d4fd39c47 100644 --- a/config/locales/crowdin/js-lt.yml +++ b/config/locales/crowdin/js-lt.yml @@ -342,21 +342,22 @@ lt: learn_about: "Sužinokite daugiau apie naujas galimybes" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - Šioje laidoje yra įvairios naujos savybės ir patobulinimai:
    + Laidoje yra naujos savybės ir patobulinimai:
    ical_sharing_modal: - title: "Prenumeruokite iCalendar" + title: "Prenumeruoti kalendorių" inital_setup_error_message: "Gaunant duomenis įvyko klaida." - description: "Jūs galite naudoti iCalendar URL, kad importuotumėte ar prenumeruotumėte šį kalendorių išoriniame kliente ir matytumėte ten aktualią darbo paketo informaciją." - warning: "Prašome nesidalinti šiuo URL su išoriniais naudotojais. Bet kas su šia nuoroda galės peržiūrėti darbo paketo detales be paskyros ar slaptažodžio." - token_name_label: "Žetono pavadinimas" - token_name_placeholder: "Įrašykite šio žetono pavadinimą, pvz. „Mano kalendoriaus aplikacija“" - token_name_description_text: "Šis pavadinimas bus naudojamas atskirti jį nuo kitų žetonų, esančių prieigos žetonų sąraše. Jį galima panaudoti tik vieną kartą kalendoriuje." + description: "Jūs galite naudoti URL (iCalendar), kad prenumeruotumėte šį kalendorių išoriniame kliente ir žiūrėti naujausią darbo paketų informaciją." + warning: "Prašome nesidalinti šiuo URL su kitais naudotojais. Bet kas su šia nuoroda galės peržiūrėti darbo paketo detales be paskyros ar slaptažodžio." + token_name_label: "Kur jūs tai naudosite?" + token_name_placeholder: "Įrašykite pavadinimą, pvz. „Telefonas“" + token_name_description_text: "Jei prenumeruojate kalendorių iš kelių įrenginių, šis pavadinimas padės atskirti juos jūsų prieigos žetonų sąraše." copy_url_label: "Kopijuoti URL" - ical_generation_error_text: "Kuriant iCal URL įvyko klaida." + ical_generation_error_text: "Kuriant kalendoriaus URL įvyko klaida." + success_message: "URL „%{name}“ buvo sėkmingai nukopijuotas į jūsų iškarpinę. Įkelkite jį į jūsų kalendoriaus klientą, kad baigtumėte prenumeratą." label_activate: "Aktyvuoti" label_assignee: 'Paskirtas' label_add_column_after: "Pridėti stulpelį po" @@ -1082,7 +1083,7 @@ lt: save_as: "Įrašyti kaip" export: "Eksportuoti" visibility_settings: "Matomumo nustatymai" - share_calendar: "Prenumeruokite iCalendar" + share_calendar: "Prenumeruoti kalendorių" page_settings: "Pervardinti vaizdą" delete: "Trinti" filter: "Filtras" @@ -1279,6 +1280,7 @@ lt: between_two_specific_dates: 'tarp dviejų konkrečių datų' legends: changes_since: 'Pakeitimai nuo' + changes_between: 'Pakeitimai tarp' now_meets_filter_criteria: 'Dabar atitinka filtro kriterijų' no_longer_meets_filter_criteria: 'Nebeatitinka filtro kriterijaus' maintained_with_changes: 'Palaikoma su pakeitimais' diff --git a/config/locales/crowdin/js-lv.yml b/config/locales/crowdin/js-lv.yml index 13a8bd6bc20..c448c0dd5fe 100644 --- a/config/locales/crowdin/js-lv.yml +++ b/config/locales/crowdin/js-lv.yml @@ -342,21 +342,22 @@ lv: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activate" label_assignee: 'Pašreizējais atbildīgais' label_add_column_after: "Add column after" @@ -1081,7 +1082,7 @@ lv: save_as: "Saglabāt kā" export: "Eksportēt" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Dzēst" filter: "Filtrēt" @@ -1274,6 +1275,7 @@ lv: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-mn.yml b/config/locales/crowdin/js-mn.yml index 57e7291c485..cace24435f3 100644 --- a/config/locales/crowdin/js-mn.yml +++ b/config/locales/crowdin/js-mn.yml @@ -342,21 +342,22 @@ mn: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activate" label_assignee: 'Assignee' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ mn: save_as: "Save as" export: "Export" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Delete" filter: "Filter" @@ -1273,6 +1274,7 @@ mn: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-ne.yml b/config/locales/crowdin/js-ne.yml index 08c3dddd203..3242731ea08 100644 --- a/config/locales/crowdin/js-ne.yml +++ b/config/locales/crowdin/js-ne.yml @@ -342,21 +342,22 @@ ne: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activate" label_assignee: 'Assignee' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ ne: save_as: "Save as" export: "Export" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Delete" filter: "Filter" @@ -1273,6 +1274,7 @@ ne: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-nl.yml b/config/locales/crowdin/js-nl.yml index 19ae3d832b7..3519f9d5fd6 100644 --- a/config/locales/crowdin/js-nl.yml +++ b/config/locales/crowdin/js-nl.yml @@ -300,28 +300,28 @@ nl: label: 'Gantt chart' quarter_label: 'Q%{quarter_number}' labels: - title: 'Label configuration' + title: 'Labelconfiguratie' bar: 'Bar labels' - left: 'Left' - right: 'Right' - farRight: 'Far right' + left: 'Links' + right: 'Rechts' + farRight: 'Uiterst rechts' description: > Select the attributes you want to be shown in the respective positions of the Gantt chart at all times. Note that when hovering over an element, its date labels will be shown instead of these attributes. button_activate: 'Show Gantt chart' button_deactivate: 'Hide Gantt chart' filter: - noneSelection: "(none)" + noneSelection: "(geen)" selection_mode: notification: 'Click on any highlighted work package to create the relation. Press escape to cancel.' zoom: - in: "Zoom in" - out: "Zoom out" - auto: "Auto zoom" - days: "Days" - weeks: "Weeks" - months: "Months" - quarters: "Quarters" - years: "Years" + in: "Inzoomen" + out: "Uitzoomen" + auto: "Automatisch inzoomen" + days: "Dagen" + weeks: "Weken" + months: "Maanden" + quarters: "Kwartalen" + years: "Jaren" description: > Select the initial zoom level that should be shown when autozoom is not available. general_text_no: "Nee" @@ -342,21 +342,22 @@ nl: learn_about: "Meer informatie over de nieuwe functies" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "URL kopiëren" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activeren" label_assignee: 'Toegewezene' label_add_column_after: "Kolom toevoegen" @@ -600,7 +601,7 @@ nl: dateAlert: 'Date alert' date_alerts: milestone_date: 'Milestone date' - overdue: 'Overdue' + overdue: 'Te laat' overdue_since: 'since %{difference_in_days}' property_today: 'is vandaag' property_is: 'is in %{difference_in_days}' @@ -672,15 +673,15 @@ nl: work_package_scheduled: 'Alle datum wijzigingen ' global: immediately: - title: 'Participating' + title: 'Deelnemend' description: 'Notifications for all activities in work packages you are involved in (assignee, accountable or watcher).' delayed: - title: 'Non-participating' + title: 'Niet-deelnemend' description: 'Additional notifications for activities in all projects.' date_alerts: title: 'Date alerts' description: 'Automatic notifications when important dates are approaching for open work packages you are involved in (assignee, accountable or watcher).' - teaser_text: 'With date alerts, you will be notified of upcoming start or finish dates so that you never miss or forget an important deadline.' + teaser_text: 'Met datummeldingen wordt u op de hoogte gebracht van de start- of einddatum, zodat u nooit een belangrijke deadline mist of vergeet.' overdue: When overdue project_specific: title: 'Project-specifieke meldingsinstellingen' @@ -889,7 +890,7 @@ nl: start_date_limited_by_relations: "Available start and finish dates are limited by relations." changing_dates_affects_follow_relations: "Changing these dates will affect dates of related work packages." click_on_show_relations_to_open_gantt: 'Click on "%{button_name}" for GANTT overview.' - show_relations: 'Show relations' + show_relations: 'Relaties tonen' ignore_non_working_days: title: 'Alleen werkdagen' description_filter: "Filter" @@ -1068,7 +1069,7 @@ nl: months: "maanden" toolbar: settings: - configure_view: "Configure view" + configure_view: "Weergave configureren" columns: "Kolommen" sort_by: "Sorteer op" group_by: "Groeperen op" @@ -1080,7 +1081,7 @@ nl: save_as: "Opslaan als" export: "Exporteren" visibility_settings: "Zichtbaarheidsinstellingen" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Hernoem weergave" delete: "Verwijderen" filter: "Filter" @@ -1099,8 +1100,8 @@ nl: text: 'Weet u zeker dat u deze actie wilt uitvoeren?' destroy_work_package: title: "Bevestig verwijdering van %{label}" - single_text: "Are you sure you want to delete the work package" - bulk_text: "Are you sure you want to delete the following %{label}?" + single_text: "Weet u zeker dat u het werkpakket wilt verwijderen" + bulk_text: "Weet u zeker dat u de volgende %{label} wilt verwijderen?" has_children: "Het werkpakket heeft %{childUnits}:" confirm_deletion_children: "Ik erken dat alle onderliggende werkpakketten van de weergegeven werkpakketten verwijderd worden." deletes_children: "Alle onderliggende werkpakketten en hun nakomelingen ook worden verwijderd." @@ -1160,7 +1161,7 @@ nl: close_search: "Sluit zoekopdracht" current_project_and_all_descendants: "In dit project + subprojecten" current_project: "In dit project" - recently_viewed: "Recently viewed" + recently_viewed: "Recent bekeken" search: "Zoeken" title: all_projects: "alle projecten" @@ -1259,20 +1260,21 @@ nl: show_changes_since: 'Toon wijzigingen sinds' baseline_comparison: 'Baseline comparison' help_description: 'Reference time zone for the baseline.' - time_description: 'In your local time: %{datetime}' + time_description: 'In uw lokale tijd: %{datetime}' time: 'Tijd' from: 'Van' to: 'Aan' drop_down: none: '-' yesterday: 'gisteren' - last_working_day: 'last working day' + last_working_day: 'laatste werkdag' last_week: 'Vorige week' last_month: 'vorige maand' - a_specific_date: 'a specific date' - between_two_specific_dates: 'between two specific dates' + a_specific_date: 'een specifieke datum' + between_two_specific_dates: 'tussen twee specifieke datums' legends: - changes_since: 'Changes since' + changes_since: 'Veranderingen sinds' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-no.yml b/config/locales/crowdin/js-no.yml index be03f411f73..328435c1b2d 100644 --- a/config/locales/crowdin/js-no.yml +++ b/config/locales/crowdin/js-no.yml @@ -342,21 +342,22 @@ learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Aktiver" label_assignee: 'Deltaker' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ save_as: "Lagre som" export: "Eksportèr" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Slett" filter: "Filter" @@ -1273,6 +1274,7 @@ between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-pl.yml b/config/locales/crowdin/js-pl.yml index 577a0f09346..0d096a59c47 100644 --- a/config/locales/crowdin/js-pl.yml +++ b/config/locales/crowdin/js-pl.yml @@ -342,21 +342,22 @@ pl: learn_about: "Dowiedz się więcej o nowych funkcjach" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - Wersja zawiera różne nowe funkcje i ulepszenia:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Aktywuj" label_assignee: 'Przypisana osoba' label_add_column_after: "Dodaj kolumnę po" @@ -1082,7 +1083,7 @@ pl: save_as: "Zapisz jako" export: "Eksportuj" visibility_settings: "Ustawienia widoczności" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Zmień nazwę widoku" delete: "Usuń" filter: "Filtr" @@ -1279,6 +1280,7 @@ pl: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-pt.yml b/config/locales/crowdin/js-pt.yml index a860d42a4d0..dbb35bc5039 100644 --- a/config/locales/crowdin/js-pt.yml +++ b/config/locales/crowdin/js-pt.yml @@ -341,21 +341,22 @@ pt: learn_about: "Saiba mais sobre os novos recursos" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - A versão contém vários recursos novos e melhorias:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Ativar" label_assignee: 'Atribuído para' label_add_column_after: "Adicionar coluna depois" @@ -1079,7 +1080,7 @@ pt: save_as: "Salvar como" export: "Exportar" visibility_settings: "Configurações de visibilidade" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Renomear exibição" delete: "Excluir" filter: "Filtro" @@ -1272,6 +1273,7 @@ pt: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-ro.yml b/config/locales/crowdin/js-ro.yml index 5e165db7195..feddf91b35d 100644 --- a/config/locales/crowdin/js-ro.yml +++ b/config/locales/crowdin/js-ro.yml @@ -341,21 +341,22 @@ ro: learn_about: "Aflați mai multe despre noile caracteristici" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - Versiunea conţine diverse caracteristici noi şi îmbunătăţiri:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activare" label_assignee: 'Executant' label_add_column_after: "Adauga o coloana dupa" @@ -1080,7 +1081,7 @@ ro: save_as: "Salvare ca" export: "Exportare" visibility_settings: "Setări de vizibilitate" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Redenumire" delete: "Ștergere" filter: "Filtrare" @@ -1275,6 +1276,7 @@ ro: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-ru.yml b/config/locales/crowdin/js-ru.yml index 1ccedcfbd45..1de127b8ea6 100644 --- a/config/locales/crowdin/js-ru.yml +++ b/config/locales/crowdin/js-ru.yml @@ -341,21 +341,22 @@ ru: learn_about: "Подробнее о новых функциях" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - Релиз содержит различные новые функции и улучшения:
    + Релиз содержит различные новые функции и улучшения:
    ical_sharing_modal: - title: "Подписаться на iCalendar" + title: "Подписаться на календарь" inital_setup_error_message: "Произошла ошибка при получении данных." - description: "Вы можете использовать URL-адрес iCalendar для импорта или подписки на этот календарь во внешнем клиенте и просматривать информацию о рабочем пакете отсюда." - warning: "Пожалуйста, не делитесь этим URL-адресом с внешними пользователями. Любой пользователь с этой ссылкой сможет просматривать детали пакета работ без учетной записи или пароля." - token_name_label: "Имя токена" - token_name_placeholder: "Введите имя для этого токена, например \"Приложение Моего календаря\"" - token_name_description_text: "Это имя будет использоваться для выделения других маркеров в списке маркеров доступа. Может быть использовано только один раз в календарь." + description: "Вы можете использовать URL-адрес (iCalendar), чтобы подписаться на этот календарь во внешнем клиенте и просматривать информацию о рабочем пакете отсюда." + warning: "Пожалуйста, не передайте этот URL другим пользователям. Любой пользователь с этой ссылкой сможет просматривать детали пакета работ без учетной записи или пароля." + token_name_label: "Где вы будете использовать это?" + token_name_placeholder: "Введите имя, например «Телефон»" + token_name_description_text: "Если вы подписаны на этот календарь с нескольких устройств, это имя поможет вам различать их в вашем списке токенов доступа." copy_url_label: "Копировать URL" - ical_generation_error_text: "Произошла ошибка при генерации URL-адреса iCal." + ical_generation_error_text: "Произошла ошибка при генерации URL-адреса календаря." + success_message: "URL-адрес \"%{name}\" был успешно скопирован в ваш буфер обмена. Вставьте его в свой клиент календаря, чтобы завершить подписку." label_activate: "Активировать" label_assignee: 'Ответственный' label_add_column_after: "Добавить столбец после" @@ -1081,7 +1082,7 @@ ru: save_as: "Сохранить как" export: "Экспортировать" visibility_settings: "Настройки видимости" - share_calendar: "Подписаться на iCalendar" + share_calendar: "Подписаться на календарь" page_settings: "Переименовать представления" delete: "Удалить" filter: "Фильтр" @@ -1278,6 +1279,7 @@ ru: between_two_specific_dates: 'между двумя определенными датами' legends: changes_since: 'Изменения с' + changes_between: 'Изменения между' now_meets_filter_criteria: 'Теперь соответствует критериям фильтра' no_longer_meets_filter_criteria: 'Больше не соответствует критериям фильтра' maintained_with_changes: 'Поддерживается с изменениями' diff --git a/config/locales/crowdin/js-rw.yml b/config/locales/crowdin/js-rw.yml index b92185b062a..b4314e428fe 100644 --- a/config/locales/crowdin/js-rw.yml +++ b/config/locales/crowdin/js-rw.yml @@ -342,21 +342,22 @@ rw: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activate" label_assignee: 'Assignee' label_add_column_after: "Add column after" @@ -1080,7 +1081,7 @@ rw: save_as: "Save as" export: "Export" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Delete" filter: "Filter" @@ -1273,6 +1274,7 @@ rw: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-si.yml b/config/locales/crowdin/js-si.yml index 392e309f729..eee03b59332 100644 --- a/config/locales/crowdin/js-si.yml +++ b/config/locales/crowdin/js-si.yml @@ -342,21 +342,22 @@ si: learn_about: "නව විශේෂාංග ගැන වැඩි විස්තර දැනගන්න" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "සක්රිය කරන්න" label_assignee: 'අස්ගිනී' label_add_column_after: "පසු තීරුව එකතු කරන්න" @@ -1080,7 +1081,7 @@ si: save_as: "ලෙස සුරකින්න" export: "අපනයන" visibility_settings: "දෘශ්යතා සැකසුම්" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "දැක්ම නැවත නම්" delete: "මකන්න" filter: "පෙරහන්" @@ -1273,6 +1274,7 @@ si: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-sk.yml b/config/locales/crowdin/js-sk.yml index 719190dc550..46756459864 100644 --- a/config/locales/crowdin/js-sk.yml +++ b/config/locales/crowdin/js-sk.yml @@ -342,21 +342,22 @@ sk: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Aktivovať" label_assignee: 'Priradené' label_add_column_after: "Pridať stĺpec po" @@ -1082,7 +1083,7 @@ sk: save_as: "Uložiť ako" export: "Exportovať" visibility_settings: "Nastavenia viditeľnosti" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Premenovať zobrazenie" delete: "Odstrániť" filter: "Filtrovať" @@ -1279,6 +1280,7 @@ sk: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-sl.yml b/config/locales/crowdin/js-sl.yml index 9df04f023e7..e16e9e32617 100644 --- a/config/locales/crowdin/js-sl.yml +++ b/config/locales/crowdin/js-sl.yml @@ -341,21 +341,22 @@ sl: learn_about: "Preberite več o novostih" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Aktiviraj" label_assignee: 'Prevzemnik' label_add_column_after: "Dodaj stolpec za" @@ -1081,7 +1082,7 @@ sl: save_as: "Shrani kot" export: "Izvozi" visibility_settings: "Nastavitve vidljivosti" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Preimenuj pogled" delete: "Izbriši" filter: "Filtriraj" @@ -1278,6 +1279,7 @@ sl: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-sr.yml b/config/locales/crowdin/js-sr.yml index 147043995d3..43d70a50c67 100644 --- a/config/locales/crowdin/js-sr.yml +++ b/config/locales/crowdin/js-sr.yml @@ -342,21 +342,22 @@ sr: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Activate" label_assignee: 'Assignee' label_add_column_after: "Add column after" @@ -1081,7 +1082,7 @@ sr: save_as: "Save as" export: "Export" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "Delete" filter: "Filter" @@ -1276,6 +1277,7 @@ sr: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-sv.yml b/config/locales/crowdin/js-sv.yml index 0cd0220ea67..ce116b7c355 100644 --- a/config/locales/crowdin/js-sv.yml +++ b/config/locales/crowdin/js-sv.yml @@ -341,21 +341,22 @@ sv: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Aktivera" label_assignee: 'Tilldelad till' label_add_column_after: "Lägg till kolumn efter" @@ -1079,7 +1080,7 @@ sv: save_as: "Spara som" export: "Exportera" visibility_settings: "Synlighet inställningar" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Byt namn på vyn" delete: "Ta bort" filter: "Filter" @@ -1272,6 +1273,7 @@ sv: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-th.yml b/config/locales/crowdin/js-th.yml index 917d86acbbe..1e7bf75c5d7 100644 --- a/config/locales/crowdin/js-th.yml +++ b/config/locales/crowdin/js-th.yml @@ -342,21 +342,22 @@ th: learn_about: "Learn more about the new features" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - The release contains various new features and improvements:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "เปิดใช้งาน" label_assignee: 'ผู้ได้รับมอบหมาย' label_add_column_after: "Add column after" @@ -1079,7 +1080,7 @@ th: save_as: "บันทึกเป็น" export: "Export" visibility_settings: "Visibility settings" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Rename view" delete: "ลบ" filter: "ตัวกรอง" @@ -1270,6 +1271,7 @@ th: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-tr.yml b/config/locales/crowdin/js-tr.yml index 2781e2bdea8..a28835867d9 100644 --- a/config/locales/crowdin/js-tr.yml +++ b/config/locales/crowdin/js-tr.yml @@ -341,21 +341,22 @@ tr: learn_about: "Yeni özellikler hakkında daha fazla bilgi edinin" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - Sürüm, çeşitli yeni özellikler ve geliştirmeler içeriyor:
    + The release contains various new features and improvements:
    ical_sharing_modal: - title: "Subscribe to iCalendar" + title: "Subscribe to calendar" inital_setup_error_message: "An error occured while fetching data." - description: "You can use the iCalendar URL to import or subscribe to this calendar in an external client and view up-to-date work package information from there." - warning: "Please don't share this URL with external users. Anyone with this link will be able to view work package details without an account or password." - token_name_label: "Token name" - token_name_placeholder: "Type a name for this token, e.g. \"My calendar application\"" - token_name_description_text: "This name will be used to distinguish it form other tokens in the access tokens list. It can only be used once per calendar." + description: "You can use the URL (iCalendar) to subscribe to this calendar in an external client and view up-to-date work package information from there." + warning: "Please don't share this URL with other users. Anyone with this link will be able to view work package details without an account or password." + token_name_label: "Where will you be using this?" + token_name_placeholder: "Type a name, e.g. \"Phone\"" + token_name_description_text: "If you subscribe to this calendar from multiple devices, this name will help you distinguish between them in your access tokens list." copy_url_label: "Copy URL" - ical_generation_error_text: "An error occured while generating the iCal URL." + ical_generation_error_text: "An error occured while generating the calendar URL." + success_message: "The URL \"%{name}\" was successfully copied to your clipboard. Paste it in your calendar client to complete the subscription." label_activate: "Etkinleştir" label_assignee: 'Atanan' label_add_column_after: "Sonrasına sütun ekle" @@ -1079,7 +1080,7 @@ tr: save_as: "Farklı Kaydet" export: "Dışarı aktar" visibility_settings: "Görünürlük ayarları" - share_calendar: "Subscribe to iCalendar" + share_calendar: "Subscribe to calendar" page_settings: "Görünümü yeniden isimlendir" delete: "Sil" filter: "Filtre" @@ -1272,6 +1273,7 @@ tr: between_two_specific_dates: 'between two specific dates' legends: changes_since: 'Changes since' + changes_between: 'Changes between' now_meets_filter_criteria: 'Now meets filter criteria' no_longer_meets_filter_criteria: 'No longer meets filter criteria' maintained_with_changes: 'Maintained with changes' diff --git a/config/locales/crowdin/js-uk.yml b/config/locales/crowdin/js-uk.yml index 3da1379ce5c..d671c22e9eb 100644 --- a/config/locales/crowdin/js-uk.yml +++ b/config/locales/crowdin/js-uk.yml @@ -342,21 +342,22 @@ uk: learn_about: "Дізнатися більше про нові функції" #Include the version to invalidate outdated translations in other locales. #Otherwise, e.g. chinese might still have the translations for 10.0 in the 12.0 release. - '12_5': + '13_0': standard: - learn_about_link: https://www.openproject.org/blog/openproject-12-5-release + learn_about_link: https://www.openproject.org/blog/openproject-13-0-release/ new_features_html: > - Випуск містить нові й поліпшені функції.
    diff --git a/frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.ts b/frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.ts index 00f12875875..39b80ed68c8 100644 --- a/frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.ts +++ b/frontend/src/app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal.ts @@ -123,7 +123,6 @@ export class QueryGetIcalUrlModalComponent extends OpModalComponent implements O return; } - let icalUrl = ''; const tokenName = (this.tokenNameForm.value as TokenNameFormValue)?.name; @@ -136,7 +135,7 @@ export class QueryGetIcalUrlModalComponent extends OpModalComponent implements O void promise .then((response:{ icalUrl:{ href:string } }) => { - this.copyToClipboardService.copy(String(response.icalUrl.href)); + this.copyToClipboardService.copy(String(response.icalUrl.href), this.I18n.t('js.ical_sharing_modal.success_message', { name: tokenName })); this.closeMe(); }) .catch((error:{ message:string }) => { diff --git a/frontend/src/app/shared/components/op-content-loader/op-principal-loading-skeleton/op-principal-loading-skeleton.component.sass b/frontend/src/app/shared/components/op-content-loader/op-principal-loading-skeleton/op-principal-loading-skeleton.component.sass index 423c2dd1e26..ce77f4c6a3a 100644 --- a/frontend/src/app/shared/components/op-content-loader/op-principal-loading-skeleton/op-principal-loading-skeleton.component.sass +++ b/frontend/src/app/shared/components/op-content-loader/op-principal-loading-skeleton/op-principal-loading-skeleton.component.sass @@ -2,6 +2,5 @@ width: 100% position: relative background: var(--body-background) - font-size: var(--card-font-size) overflow: hidden height: 80px \ No newline at end of file diff --git a/frontend/src/app/shared/components/op-content-loader/op-wp-loading-skeleton/op-wp-loading-skeleton.component.sass b/frontend/src/app/shared/components/op-content-loader/op-wp-loading-skeleton/op-wp-loading-skeleton.component.sass index 66e72f9952a..69b2500b2ad 100644 --- a/frontend/src/app/shared/components/op-content-loader/op-wp-loading-skeleton/op-wp-loading-skeleton.component.sass +++ b/frontend/src/app/shared/components/op-content-loader/op-wp-loading-skeleton/op-wp-loading-skeleton.component.sass @@ -6,7 +6,6 @@ position: relative box-shadow: 1px 1px 3px 0px lightgrey background: var(--body-background) - font-size: var(--card-font-size) overflow: hidden height: 80px diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts index 9b9cbbf6fad..411675daa2f 100644 --- a/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts +++ b/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts @@ -64,7 +64,8 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger { private loadingPromise:PromiseLike; - constructor(readonly elementRef:ElementRef, + constructor( + readonly elementRef:ElementRef, readonly opContextMenu:OPContextMenuService, readonly opModalService:OpModalService, readonly wpListService:WorkPackagesListService, @@ -72,7 +73,8 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger { readonly states:States, readonly injector:Injector, readonly querySpace:IsolatedQuerySpace, - readonly I18n:I18nService) { + readonly I18n:I18nService, + ) { super(elementRef, opContextMenu); } @@ -274,6 +276,20 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger { return true; }, }, + { + // Calendar sharing modal + hidden: !this.showCalendarSharingOption, + disabled: this.authorisationService.cannot('query', 'icalUrl'), + linkText: this.I18n.t('js.toolbar.settings.share_calendar'), + icon: 'icon-link', // TODO: find sharing icons + onClick: () => { + if (this.authorisationService.can('query', 'icalUrl')) { + this.opModalService.show(QueryGetIcalUrlModalComponent, this.injector); + } + + return true; + }, + }, { // Export query disabled: this.authorisationService.cannot('work_packages', 'representations'), @@ -313,20 +329,6 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger { icon: 'icon-custom-fields', onClick: () => false, }, - { - // Calendar sharing modal - hidden: !this.showCalendarSharingOption, - disabled: this.authorisationService.cannot('query', 'icalUrl'), - linkText: this.I18n.t('js.toolbar.settings.share_calendar'), - icon: 'icon-link', // TODO: find sharing icons - onClick: () => { - if (this.authorisationService.can('query', 'icalUrl')) { - this.opModalService.show(QueryGetIcalUrlModalComponent, this.injector); - } - - return true; - }, - }, ]; } } diff --git a/frontend/src/app/shared/components/option-list/option-list.sass b/frontend/src/app/shared/components/option-list/option-list.sass index b0bc7fe593e..bf3ccf7197d 100644 --- a/frontend/src/app/shared/components/option-list/option-list.sass +++ b/frontend/src/app/shared/components/option-list/option-list.sass @@ -1,7 +1,6 @@ .op-option-list display: flex flex-direction: column - font-size: 1rem // TODO: remove the [type] selector once globally overwriting styles are removed &--input[type] @@ -33,7 +32,7 @@ &-title font-weight: bold - font-size: 14px &-description + font-weight: normal font-size: 12px diff --git a/frontend/src/app/shared/components/storages/file-picker-modal/file-picker-modal.component.html b/frontend/src/app/shared/components/storages/file-picker-modal/file-picker-modal.component.html index 73fbed13b29..a5992e14384 100644 --- a/frontend/src/app/shared/components/storages/file-picker-modal/file-picker-modal.component.html +++ b/frontend/src/app/shared/components/storages/file-picker-modal/file-picker-modal.component.html @@ -47,18 +47,18 @@ - + -
    +
    -
    +
    -
    +
    @@ -62,7 +62,7 @@ -
    +
    this.i18n.t('js.storages.files.dragging_many_files', { storageType }), + draggingFolder: (storageType:string):string => this.i18n.t('js.storages.files.dragging_folder', { storageType }), uploadingLabel: this.i18n.t('js.label_upload_notification'), }, dropBox: { @@ -519,11 +520,15 @@ export class StorageComponent extends UntilDestroyedMixin implements OnInit, OnD this.dragging = 0; const files = event.dataTransfer.files; - if (files.length !== 1) { + const draggingManyFiles = files.length !== 1; + const isDirectory = event.dataTransfer.items[0].webkitGetAsEntry()?.isDirectory; + if (draggingManyFiles || isDirectory) { this.storageType .pipe(first()) .subscribe((storageType) => { - const toast = this.text.toast.draggingManyFiles(storageType); + const toast = draggingManyFiles + ? this.text.toast.draggingManyFiles(storageType) + : this.text.toast.draggingFolder(storageType); this.toastService.addError(toast); }); return; diff --git a/frontend/src/app/shared/components/toaster/toast.component.html b/frontend/src/app/shared/components/toaster/toast.component.html index b9480369aa0..499dfbb1d21 100644 --- a/frontend/src/app/shared/components/toaster/toast.component.html +++ b/frontend/src/app/shared/components/toaster/toast.component.html @@ -1,16 +1,16 @@
    -``` diff --git a/frontend/src/global_styles/content/_copy_to_clipboard.lsg b/frontend/src/global_styles/content/_copy_to_clipboard.lsg deleted file mode 100644 index a6792e382c0..00000000000 --- a/frontend/src/global_styles/content/_copy_to_clipboard.lsg +++ /dev/null @@ -1,25 +0,0 @@ -# Copy to clipboard - -``` -
    -
    -
    -

    Toolbar

    -
      -
    • -
      - git clone -
      - -
      - -
      -
    • -
    -
    -
    -``` diff --git a/frontend/src/global_styles/content/_datepicker.lsg b/frontend/src/global_styles/content/_datepicker.lsg deleted file mode 100644 index d3b05f536a9..00000000000 --- a/frontend/src/global_styles/content/_datepicker.lsg +++ /dev/null @@ -1,6 +0,0 @@ -## Datepicker - -``` - - -``` diff --git a/frontend/src/global_styles/content/_datepicker.sass b/frontend/src/global_styles/content/_datepicker.sass index fabf77c1195..18bf718f3e2 100644 --- a/frontend/src/global_styles/content/_datepicker.sass +++ b/frontend/src/global_styles/content/_datepicker.sass @@ -26,7 +26,6 @@ // See COPYRIGHT and LICENSE files for more details. //++ -@import '../../global_styles/openproject/variables' $datepicker--selected-border-radius: 5px @mixin disabled-day diff --git a/frontend/src/global_styles/content/_editable_toolbar.sass b/frontend/src/global_styles/content/_editable_toolbar.sass index 06de54a39d9..411ed01f940 100644 --- a/frontend/src/global_styles/content/_editable_toolbar.sass +++ b/frontend/src/global_styles/content/_editable_toolbar.sass @@ -12,7 +12,7 @@ .title-container.-small .editable-toolbar-title--fixed - font-size: 1.2rem + font-size: 1rem line-height: 32px .toolbar--editable-toolbar diff --git a/frontend/src/global_styles/content/_enterprise.sass b/frontend/src/global_styles/content/_enterprise.sass index fe3aab573aa..3c777eb964d 100644 --- a/frontend/src/global_styles/content/_enterprise.sass +++ b/frontend/src/global_styles/content/_enterprise.sass @@ -18,6 +18,10 @@ box-shadow: 4px 4px 10px rgb(0 0 0 / 15%) border-radius: 25px + a.button, + a.button--link + text-decoration: none + .upsale-actions display: flex flex-wrap: wrap @@ -37,8 +41,15 @@ display: flex justify-content: flex-end + a.button, + a.button--link + text-decoration: none + margin: 0.625rem 0.625rem 0 0 + .widget-box--blocks--upsale-info-button - margin: 0.5rem 1rem 0 0 + display: flex + justify-content: center + align-items: center .upsale-icon &_highlighted @@ -47,6 +58,10 @@ .widget-box--blocks--upsale-title font-weight: 400 font-size: 20px + margin-top: 10px + display: flex + +.widget-box--blocks--upsale-text line-height: 24px margin-top: 10px diff --git a/frontend/src/global_styles/content/_forms.lsg b/frontend/src/global_styles/content/_forms.lsg deleted file mode 100644 index ce40a1df4d7..00000000000 --- a/frontend/src/global_styles/content/_forms.lsg +++ /dev/null @@ -1,1233 +0,0 @@ -# Forms - -## Forms: Standard style - -``` - -
    - -
    -
    - -
    -
    -
    - -
    - - - -``` - -## Forms: Bordered style - -``` -
    -
    - -
    -
    - -
    -
    -
    - -
    - - -
    -``` - -## Forms: Bordered compressed style (less padding) - -``` -
    -
    - -
    -
    - -
    -
    -
    - -
    - - -
    -``` - -## Forms: Bordered style - Danger zone - -``` -
    -
    -

    - Delete account foo.bar@openproject.org -

    -

    - Your account will be deleted from the system. Therefore, you will no longer be able to log in with your current credentials. If you choose to become a user of this application again, you can do so by using the means this application grants. -

    -

    - Of the data you created as much as possible will be deleted. Note however, that data like work packages and wiki entries can not be deleted without impeding the work of the other users. Such data is hence reassigned to an account called "Deleted user". -

    -

    - - Deleting your account is an irreversible action. -

    -

    - Enter your login foo.bar@openproject.org to verify the deletion. -

    -
    - - - - Cancel - -
    -
    -
    -``` - -## Forms: Standard layout - -``` -@full-width - -
    -
    -
    - -
    -
    - -
    -
    -
    - Write anything you like. -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    - - Add 5 - -
    -
    - Any number from 1 to 10! -
    -
    -
    - -
    -
    - -
    -
    -
    - Write more about anything. -
    -
    -
    -
    -
    - -
    -
    -
    - This field has no label, which means you really can write what you like. -
    -
    -
    -
    -
    - -
    -
    -
    - This field also has no label, but takes up the full width. -
    -
    -
    -
    -``` - -## Forms: Standard layout, wide labels - -``` -@full-width - -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    - Your personal email address. -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - per item -
    -
    -
    - The more, the better -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    - Write more about anything. -
    -
    -
    -
    - - -
    -
    - Selecting these option might be considered a dangerous operation. -
    -
    -
    -
    -``` - -## Forms: Multiple fields per row - -``` -@full-width - -
    -
    - Wichtige Daten -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    - One never lies about one's age. -
    -
    -
    - -
    -
    - -
    -
    Liter
    -
    pro
    -
    - -
    -
    auf
    -
    - -
    -
    -
    -
    -
    Colors:
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    -
    -``` - -## Forms: Vertical layout - -``` -@full-width - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    - Write anything you like. -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - unit -
    -
    -
    - Any number from 1 to 10! -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    - Write more about anything. -
    -
    -
    -
    -
    -``` - -## Forms: Sections and fieldsets - -### standard - -``` -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -

    Advanced information

    -
    - -
    -
    - -
    -
    -
    -
    -
    - - Even more advanced information - -
    - -
    -
    - -
    -
    -
    -
    -
    -``` - -### with controls - -``` -
    -
    - - Various information - -
    - - (Check all | Uncheck all) - -
    -
    - -
    - - -
    -
    -
    -
    -``` - -### collapsible - -``` -
    -
    - - Less important information - - -
    -
    - - More important information - -
    - -
    -
    - -
    -
    -
    -
    -
    -``` - -## Forms: Column layout - -``` -@full-width - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    - Write anything you like. -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    - - Add 5 - -
    -
    - Any number from 1 to 10! -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    - Write more about anything. -
    -
    -
    -
    -
    - -
    -
    -
    - This field has no label, which means you really can write what you like. -
    -
    -
    -
    -
    -``` - -## Forms: Width categories - -The modifier classes **-middle**, **-wide**, ... can be applied to the form-[input type]-container to limit the width of the element. Please note, that this only limits the max-width of the element. The fields will become slimmer if less space is available. Also note, that the max-width is specified as an absolute value. Without a modifier, the form field will take the full width available. - -``` -@full-width - -
    -
    -

    Input type "text-field"

    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - - -
    -

    Input type "text-area"

    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - - -
    -

    Input type "select"

    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -``` - -# Forms: Attachment fieldsets - -``` -
    - - Filesexpanded - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    - -
    -
    -
    - - Add another file - (Maximum size: 5 MB) - -
    -
    -``` - -# Forms: Text fields - -## Default text fields - -### Standalone - -``` - - - -``` - -## Text field sizes - -``` - - - - - -``` - -## Plain text fields - -_with no classes applied (uses default Foundation form styling)_ - -``` - - - -``` - -# Forms: Text areas - -## Default text areas - -### Standalone - -``` - -``` - -## Plain text areas - -_with no classes applied (uses default Foundation form styling)_ - -``` - -``` - -# Forms: Checkboxes - -## Default checkboxes - -### Standalone - -``` - - - -``` - -### Within a form - -``` -
    -
    - -
    -
    - -
    -
    -
    -
    -``` - -### Multiple, within a form - -``` -
    -
    - -
    - - - -
    -
    -
    -``` - -# Forms: Radio buttons - -## Within a form, multiple - -``` -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -``` - -# Forms: Select - -## Default selects - -### Standalone - -``` - -``` - -``` - -``` - -``` - -``` - -### Within a form - -``` -
    -
    - -
    -
    - -
    -
    -
    - Oranges are rich in Vitamin C. Eat more than two! -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -``` - -## Select Sizes - -``` - - - - - -``` - -## Narrow select - -_By default, a `form--select` will take the full width of its container element. -In most cases it is recommended to apply a width to the container element, but -in certain circumstances the `-narrow` variant may be preferable._ - -``` - - - -``` - -## Plain selects - -_with no classes applied (uses default Foundation form styling)_ - -``` - -``` - -# Forms: Checkbox Matrices - -``` - - - - - - - - - - - - - - - - - - - - - - - - - -
    Attribute nameUser accessAdmin access
    - Project - - - - -
    - Type - - - - -
    - Parent - - - - -
    -``` - -# Forms: Inline buttons - -``` -
    -
    -
    - - - -
    -
    -
    -``` diff --git a/frontend/src/global_styles/content/_forms.sass b/frontend/src/global_styles/content/_forms.sass index b3a3ede66ae..7f6720fe003 100644 --- a/frontend/src/global_styles/content/_forms.sass +++ b/frontend/src/global_styles/content/_forms.sass @@ -26,8 +26,6 @@ // See COPYRIGHT and LICENSE files for more details. //++ -@import "../openproject/variables" - $form--field-types: (text-field, text-area, select, check-box, radio-button, range-field, search, file, date-picker) $base-line-height: 1.5 $form-label-color: var(--body-font-color) @@ -35,7 +33,7 @@ $form-label-color: var(--body-font-color) %input-style border: var(--content-form-input-border) border-radius: 2px - font-size: 0.9rem + font-size: var(--body-font-size) &:hover, &:focus border: var(--content-form-input-hover-border) @@ -229,7 +227,7 @@ hr %form--fieldset-legend-or-section-title color: #4d4d4d - font-size: 1rem + font-size: var(--body-font-size) font-weight: bold line-height: 1.8 text-transform: uppercase @@ -273,7 +271,7 @@ fieldset.form--fieldset float: right text-align: right color: #4d4d4d - font-size: 1rem + font-size: var(--body-font-size) font-style: italic line-height: 1.8 margin-top: -1.8rem @@ -322,6 +320,9 @@ fieldset.form--fieldset &.-visible-overflow .form--field-container overflow: visible + &.-align-start + align-items: start + &.-vertical, .form.-vertical & @include grid-orient(vertical) @@ -429,6 +430,9 @@ fieldset.form--fieldset flex-grow: 2 align-items: stretch + .option-label .option-label--select + margin-left: $spot-spacing-1 + &.-wrap-around display: flex flex-wrap: wrap @@ -450,6 +454,10 @@ fieldset.form--fieldset > *:last-child margin-right: auto + &.-enterprise-restricted + flex-direction: column + align-items: start + .form--field.-visible-overflow & overflow: visible @@ -595,7 +603,7 @@ fieldset.form--fieldset select line-height: 100% margin-bottom: 0 - font-size: 0.9rem + font-size: var(--body-font-size) &:hover, &:focus, &:active border-color: #999 diff --git a/frontend/src/global_styles/content/_icon_control.lsg b/frontend/src/global_styles/content/_icon_control.lsg deleted file mode 100644 index 60268af6c48..00000000000 --- a/frontend/src/global_styles/content/_icon_control.lsg +++ /dev/null @@ -1,55 +0,0 @@ -# Icon Controls - -``` -
    - - - Star - - - Nur e kian landonomo, ol ind duona anstataŭa. -
    -
    - - - Unstar - - - Far ki aliam samideano noniliono. -
    - -
    - - - Watch - - - Περπετυα περσεκυερις υθ ηας -
    -
    - - - Unwatch - - - Ηας παρτεμ λεγενδως δεφινιθιονεμ νο, συμ ευ λαυδεμ εσεντ εκυιδεμ -
    - -
    - - - Group - - - Group -
    -
    - - - - - - Leave the Group -
    - -``` diff --git a/frontend/src/global_styles/content/_in_place_editing.lsg b/frontend/src/global_styles/content/_in_place_editing.lsg deleted file mode 100644 index bc1d8a41dc6..00000000000 --- a/frontend/src/global_styles/content/_in_place_editing.lsg +++ /dev/null @@ -1,19 +0,0 @@ -# In place editing - -## In place editing: Title - -``` -
    -
    - -
    - -
    -
    -
    -
    -``` diff --git a/frontend/src/global_styles/content/_index.sass b/frontend/src/global_styles/content/_index.sass index eea1f9a0652..ca11404f0dc 100644 --- a/frontend/src/global_styles/content/_index.sass +++ b/frontend/src/global_styles/content/_index.sass @@ -14,6 +14,7 @@ @import custom_logo @import toast @import toast_mobile +@import tiles @import links @import loading_indicator @import enterprise diff --git a/frontend/src/global_styles/content/_info_boxes.lsg b/frontend/src/global_styles/content/_info_boxes.lsg deleted file mode 100644 index efbce59d68c..00000000000 --- a/frontend/src/global_styles/content/_info_boxes.lsg +++ /dev/null @@ -1,73 +0,0 @@ -# Info boxes - -## Simple info boxes -``` -
    -

    Heading

    -
    -
    -

    Box 1

    -
    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    -
    -
    -
    -

    Box 2

    -
    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    -
    -
    -
    -

    Box 3

    -
    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    -
    -
    -
    -
    -``` - -## Centered with image and links -``` -
    -

    Heading

    -
    -
    - -

    Box 1

    -
    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    - -
    -
    -
    - -

    Box 2

    -
    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    - -
    -
    -
    - -

    Box 3

    -
    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    - -
    -
    -
    -
    -``` \ No newline at end of file diff --git a/frontend/src/global_styles/content/_loading_indicator.lsg b/frontend/src/global_styles/content/_loading_indicator.lsg deleted file mode 100644 index 04c4a3835c2..00000000000 --- a/frontend/src/global_styles/content/_loading_indicator.lsg +++ /dev/null @@ -1,25 +0,0 @@ -# Loading indicator - -```html - - - - - - -``` - - -To block the entire page, wrap the element in a `div.loading-indicator--background` - - -``` html -
    - - - - - - -
    -``` diff --git a/frontend/src/global_styles/content/_loading_indicator.sass b/frontend/src/global_styles/content/_loading_indicator.sass index 83b9aef6ba3..065914dfa5d 100644 --- a/frontend/src/global_styles/content/_loading_indicator.sass +++ b/frontend/src/global_styles/content/_loading_indicator.sass @@ -104,3 +104,7 @@ $loading-indicator-height: 70px transform: rotate(45deg) animation-name: svg_ani-2 animation-delay: 2130ms + + &_in-toast + top: 0 + left: 50px diff --git a/frontend/src/global_styles/content/_modal.lsg b/frontend/src/global_styles/content/_modal.lsg deleted file mode 100644 index b1fd8bf7f9e..00000000000 --- a/frontend/src/global_styles/content/_modal.lsg +++ /dev/null @@ -1,84 +0,0 @@ -# Modals - -## Modals: Standard style - -``` -
    -
    - Export -
    -
    - -
    -
    - - -
    -
    -
    - -``` - -## Modals: Danger zone (highlighted buttons) - -``` -
    -
    - - Confirm deletion of Work Package -
    -
    -
    -

    Are you sure you want to delete the following work package?

    -

    ...

    -
    -
    -
    - - -
    -
    -
    -``` diff --git a/frontend/src/global_styles/content/_pagination.lsg b/frontend/src/global_styles/content/_pagination.lsg deleted file mode 100644 index 93d610fa707..00000000000 --- a/frontend/src/global_styles/content/_pagination.lsg +++ /dev/null @@ -1,32 +0,0 @@ -# Pagination - -``` -
    - -
    - -
    -
    - -``` diff --git a/frontend/src/global_styles/content/_project_status.sass b/frontend/src/global_styles/content/_project_status.sass index 16a4b4ea32a..626e0b59fc4 100644 --- a/frontend/src/global_styles/content/_project_status.sass +++ b/frontend/src/global_styles/content/_project_status.sass @@ -21,7 +21,6 @@ $project-status-deep-green: #007528 line-height: 44px input font-weight: bold - font-size: 16px text-transform: uppercase .ng-clear-wrapper diff --git a/frontend/src/global_styles/content/_simple_filters.sass b/frontend/src/global_styles/content/_simple_filters.sass index 4783b20527e..960175494c9 100644 --- a/frontend/src/global_styles/content/_simple_filters.sass +++ b/frontend/src/global_styles/content/_simple_filters.sass @@ -26,11 +26,9 @@ // See COPYRIGHT and LICENSE files for more details. //++ -$filters--background-color: var(--gray-light) !default $filters--border-color: var(--gray) !default %filters--container - background-color: $filters--background-color border: 1px solid $filters--border-color legend diff --git a/frontend/src/global_styles/content/_slide_toggle.lsg b/frontend/src/global_styles/content/_slide_toggle.lsg deleted file mode 100644 index 071a7816975..00000000000 --- a/frontend/src/global_styles/content/_slide_toggle.lsg +++ /dev/null @@ -1,9 +0,0 @@ -# Slide toggle checkbox - -``` - - -``` diff --git a/frontend/src/global_styles/content/_table.lsg b/frontend/src/global_styles/content/_table.lsg deleted file mode 100644 index 0ec9c0e7f1d..00000000000 --- a/frontend/src/global_styles/content/_table.lsg +++ /dev/null @@ -1,405 +0,0 @@ -# Table component - -## style emulation from -[WP tables](http://localhost:8080/assets/css/styleguide.html#with-work-packages) - -``` -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    - - First - -
    -
    -
    -
    -
    - - Second - -
    -
    -
    -
    -
    - - Third - -
    -
    -
    -
    -
    - - Fourth - -
    -
    -
    -
    -
    - - Fifth - -
    -
    -
    Lorem ipsum dolor sit amet, consectetur adipiscing elitNullam a sem et metus congue placerat.Mauris ut augue viverra, consequat eros eu, maximus quam.Maecenas elementum orci a varius suscipit.Nunc molestie neque sit amet eros semper dapibus.
    Nullam a sem et metus congue placerat.Lorem ipsum dolor sit amet, consectetur adipiscing elitMauris ut augue viverra, consequat eros eu, maximus quam.Maecenas elementum orci a varius suscipit.Nunc molestie neque sit amet eros semper dapibus.
    Nullam a sem et metus congue placerat.Mauris ut augue viverra, consequat eros eu, maximus quam.Lorem ipsum dolor sit amet, consectetur adipiscing elitMaecenas elementum orci a varius suscipit.Nunc molestie neque sit amet eros semper dapibus.
    Nullam a sem et metus congue placerat.Mauris ut augue viverra, consequat eros eu, maximus quam.Maecenas elementum orci a varius suscipit.Lorem ipsum dolor sit amet, consectetur adipiscing elitNunc molestie neque sit amet eros semper dapibus.
    Nullam a sem et metus congue placerat.Mauris ut augue viverra, consequat eros eu, maximus quam.Maecenas elementum orci a varius suscipit.Lorem ipsum dolor sit amet, consectetur adipiscing elitNunc molestie neque sit amet eros semper dapibus.
    - -
    -
    - -``` - -## Text overflow / ellipse styles - -Use `-no-ellipse` class to a TD element to disable the text-overflow applied usually. - -``` -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    - - Content wraps in new lines - -
    -
    -
    -
    -
    - - clipped - -
    -
    -
    -
    -
    - - Third - -
    -
    -
    -
    -
    - - Fourth - -
    -
    -
    -
    -
    - - Fifth - -
    -
    -
    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus porta lorem congue lacus euismod egestas. Mauris odio orci, semper sed molestie viverra, vestibulum et nisi. - Nullam a sem et metus congue placerat. Text will overflow once max-width is exceededMauris ut augue viverra, consequat eros eu, maximus quam.Maecenas elementum orci a varius suscipit.Nunc molestie neque sit amet eros semper dapibus.
    - -
    -
    - -``` - - -## with footer - -``` - - - -``` - - -## with buttons - -``` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    - - Email - -
    -
    -
    -
    -
    - Roles -
    -
    -
    -
    -
    - Status -
    -
    -
    -
    -
    - member@example.net - Memberactive - - -
    - verylongemail_example@example.com - Memberactive - - -
    - member@example.net - Adminactive - - -
    - -``` - - -## with no content - -``` -
    -
    - - - Nothing to display - -
    -

    Either nothing have been created or everything is filtered out.

    -
    -
    -
    -``` diff --git a/frontend/src/global_styles/content/_table.sass b/frontend/src/global_styles/content/_table.sass index 864db51a2ec..d1dd68a7eca 100644 --- a/frontend/src/global_styles/content/_table.sass +++ b/frontend/src/global_styles/content/_table.sass @@ -329,7 +329,7 @@ thead.-sticky th padding-left: 15px #startDate - margin-left: 24px + margin-left: $work-package--start-date-display-field-padding-left .generic-table--sort-header-outer .dropdown-indicator diff --git a/frontend/src/global_styles/content/_tabs.lsg b/frontend/src/global_styles/content/_tabs.lsg deleted file mode 100644 index 52215e7ba84..00000000000 --- a/frontend/src/global_styles/content/_tabs.lsg +++ /dev/null @@ -1,51 +0,0 @@ -# Scrollable tabs - -``` -
    -
    - - - -
    -
    -``` - -The height of the tabs section can be reduced applying the `-narrow` modification class. - -``` -
    -
    - - - -
    -
    -``` diff --git a/frontend/src/global_styles/content/_tabular.lsg b/frontend/src/global_styles/content/_tabular.lsg deleted file mode 100644 index 38896277e94..00000000000 --- a/frontend/src/global_styles/content/_tabular.lsg +++ /dev/null @@ -1 +0,0 @@ -# Tabular diff --git a/frontend/src/global_styles/content/_tiles.sass b/frontend/src/global_styles/content/_tiles.sass new file mode 100644 index 00000000000..e7d6913b941 --- /dev/null +++ b/frontend/src/global_styles/content/_tiles.sass @@ -0,0 +1,83 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) 2012-2023 the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See COPYRIGHT and LICENSE files for more details. +//++ + +.op-tile-block + $block: & + display: grid + grid-template-rows: repeat(minmax(200px, auto)) + grid-template-columns: auto auto + grid-gap: 1rem + + &--tile + border-radius: 10px + display: grid + grid-template: auto / 1fr auto + grid-row-gap: 5px + justify-items: left + background: #f7fafc + min-height: 150px + padding: 1rem + border: 1px solid var(--button--border-color) + + &:disabled + background: #fafafa + + &:hover + text-decoration: none + border: 1px solid grey + border-radius: 10px !important + cursor: pointer + + &.-disabled + @include button-disabled + + &--content + display: flex + column-gap: 0.75rem + + .form--radio-button + margin-top: 0.75rem + + &--image + display: block + margin-top: auto + margin-bottom: auto + + &--title + padding-top: 0 + padding-bottom: 5px + color: var(--primary-color-dark) + display: block + text-align: left + font-weight: bolder + font-size: large + + &--description + text-align: left + width: 90% + font-weight: normal diff --git a/frontend/src/global_styles/content/_toast.lsg b/frontend/src/global_styles/content/_toast.lsg deleted file mode 100644 index 3b56d51cded..00000000000 --- a/frontend/src/global_styles/content/_toast.lsg +++ /dev/null @@ -1,127 +0,0 @@ -# Notifications - -``` -
    -
    -

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quia laudantium ea delectus incidunt accusantium repudiandae deserunt excepturi non esse vero distinctio et reprehenderit, cupiditate quidem consectetur rerum iste magnam voluptatibus.

    -
    -
    -``` - -## Info - -``` -
    -
    -

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quia laudantium ea delectus incidunt accusantium repudiandae deserunt excepturi non esse vero distinctio et reprehenderit, cupiditate quidem consectetur rerum iste magnam voluptatibus.

    -
    -
    -``` - -## Error - -``` -
    - -
    -

    An error occurred, here are the facts:

    -
      -
    • Fact 1: You made a mistake
    • -
    • Fact 2: You really made a mistake
    • -
    • Fact 3: You should fix it
    • -
    - -
    -
    -``` - -## Warning - -``` -
    - -
    -

    This is a warning. You may ignore it, but bad things might happen.

    -
    -
    -``` - -## Success - -``` -
    - -
    -

    Successful update. A link to the past

    -
    -
    -``` - -## Stackable - - -``` -
    - -
    -
    -
    -
    - - -``` - -## Upload toasters - -### Upload progress - -``` -
    -
    - Uploading files... -
      - -
    • - Awesome_Landscape.png - 70% -
    • -
      -
    -
    -
    -``` - - -### Upload progress done - -``` -
    -
    - Uploading files... -
    -
    -
      - -
    • - Awesome_Landscape.png - -
    • -
    • - Awesome_Landscape2.png - -

      Something went wrong!...

      -
    • -
      -
    -
    -
    -
    -
    -``` diff --git a/frontend/src/global_styles/content/_toast.sass b/frontend/src/global_styles/content/_toast.sass index 77698190800..3461053edb6 100644 --- a/frontend/src/global_styles/content/_toast.sass +++ b/frontend/src/global_styles/content/_toast.sass @@ -31,7 +31,7 @@ //nm - toaster messages //pb - progress bar -$nm-font-size: rem-calc(13px) +$nm-font-size: var(--body-font-size) $nm-border-radius: rem-calc(2px) $nm-box-padding: rem-calc(10px 35px 10px 35px) $nm-toaster-width: rem-calc(550) @@ -129,11 +129,14 @@ $nm-upload-box-padding: rem-calc(15) rem-calc(25) &.-error, &.-success, &.-warning, - &.-info + &.-info, + &.-loading padding: $nm-box-padding &.-ee-upsale z-index: 1 + padding: 1rem + margin-top: 1rem &.-left-margin margin-left: 20px @@ -166,6 +169,9 @@ $nm-upload-box-padding: rem-calc(15) rem-calc(25) @include messages-icon @extend %nm-icon-info + &.-loading + @extend %info-placeholder + &.-ee-upsale @extend %info-placeholder @@ -262,9 +268,7 @@ $nm-upload-box-padding: rem-calc(15) rem-calc(25) border-radius: $nm-pb-border-radius .op-toast.-loading - width: 150px - margin: 0 auto - + width: 400px .op-toast--content display: flex align-items: center diff --git a/frontend/src/global_styles/content/_tooltips.lsg b/frontend/src/global_styles/content/_tooltips.lsg deleted file mode 100644 index 54092a1ae95..00000000000 --- a/frontend/src/global_styles/content/_tooltips.lsg +++ /dev/null @@ -1,133 +0,0 @@ -# Tooltips - -Adds tooltips to arbitrary elements. - -## Simple Tooltips - -These can contain simple texts. - -Attention: - -* They are not suitable for HTML within the Tooltip. -* Also, if the are already :before or :after CSS rules for -the decorated element, things will break as these rules will get overwritten. - -### Right - -``` - - - -``` - -### Bottom - -``` - - - -``` - -### Left - -``` - - - -``` - -### Top - -``` - - - -``` - -## Examples - -### Form elements - -``` -
    -
    - -
    -
    - -
    -
    - - - -
    -
    -
    -
    - -
    -
    - -
    -
    - - - -
    -
    -
    -
    - -
    -
    - -
    -
    - - - -
    -
    -
    -
    - -
    -
    - -
    -
    - - - -
    -
    -
    -
    -``` - -Note that the tabindex has to be set manually on the `` and not the containing element. `tabindex="0"` makes the item tabbable at all. - -### HTML tooltips - -``` - - - -
    -

    The content of an HTML tooltip.

    - OpenProject -
    -``` - -### Inline text - -``` -

    Title

    -

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere quibusdam sit voluptas illo error reiciendis non nisi necessitatibus architecto beatae, ea quos sint consectetur repellat aliquid. Ducimus provident totam pariatur.

    -``` diff --git a/frontend/src/global_styles/content/_user.lsg b/frontend/src/global_styles/content/_user.lsg deleted file mode 100644 index deda13d9883..00000000000 --- a/frontend/src/global_styles/content/_user.lsg +++ /dev/null @@ -1,26 +0,0 @@ -# User Avatars - -``` - -Standard:
    - -

    -Standard Mini:
    - -

    -Default:
    -
    OA
    -

    -Default Medium:
    -
    OA
    -

    -Default Mini:
    -
    OA
    -

    -Gravatar:
    - -

    -Gravatar Mini:
    - - -``` diff --git a/frontend/src/global_styles/content/_widget_box.lsg b/frontend/src/global_styles/content/_widget_box.lsg deleted file mode 100644 index 3cafdd2a184..00000000000 --- a/frontend/src/global_styles/content/_widget_box.lsg +++ /dev/null @@ -1,108 +0,0 @@ -# Widget Boxes - -``` -@full-width - -
    -
    - - -
    -

    -
    -

    Widget Box

    -
    -

    -

    This widget box can be used to display content belonging to one subject.

    - -
    - - -
    -

    -
    -

    Widget Box 2

    -
    -

    - -
    - - -
    -

    -
    -

    Widget Box 3

    -
    -

    -
    -

    - Lorem ipsum dolor sit amet, his ei propriae suscipit. - Sit in atqui accumsan ponderum, eum ut luptatum lobortis, has ei tota illud detraxit. -

    -
    - -
    - - -
    -

    -
    -

    Widget Box 4

    -
    -

    -
      -
    • Enum1
    • -
    • Enum2
    • -
    • Enum3
    • -
    - -
    - -
    -
    -``` diff --git a/frontend/src/global_styles/content/_widget_box.sass b/frontend/src/global_styles/content/_widget_box.sass index 1a244cc28ff..ad9c9d61e60 100644 --- a/frontend/src/global_styles/content/_widget_box.sass +++ b/frontend/src/global_styles/content/_widget_box.sass @@ -127,7 +127,7 @@ $widget-box--enumeration-width: 20px .editable-toolbar-title--fixed, .toolbar--editable-toolbar color: #5F5F5F - font-size: 16px + font-size: var(--body-font-size) letter-spacing: 1px text-transform: uppercase font-weight: bold diff --git a/frontend/src/global_styles/content/_wiki.lsg b/frontend/src/global_styles/content/_wiki.lsg deleted file mode 100644 index d8fbd25e410..00000000000 --- a/frontend/src/global_styles/content/_wiki.lsg +++ /dev/null @@ -1,358 +0,0 @@ -# Wiki - -Wiki-syntax is used for most textarea-fields within OpenProject. The users have several options to style text. - -## Container - -``` -
    - -
    -``` - -## Paragraph - -``` -
    -

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Fugit sed cum quam obcaecati eius nisi tenetur tempora odio minus nulla rerum hic, itaque nam dolorum vel fuga quibusdam, praesentium unde!

    - -

    Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.

    - -

    Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.

    - -

    Right aligned - Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.

    - -

    Centered - Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.

    -
    -``` - -## Headings - -``` -
    -

    - Headline H1 - -

    - -

    - Headline H2 - -

    - -

    - Headline H3 - -

    - -

    - Headline H4 - -

    - -
    - Headline H5 -
    - -
    - Headline H6 -
    -
    -``` - -Note: Only headings to level ***three*** are supported in the wiki toolbar at the moment. Up to level ***four***, an anchor link is added. - -## Table of contents - -``` -
    -
    - - Table of Contents - -
    - -
    -
    -
    -``` - - -Note: Only headings to level ***four*** are considered in the table of contents. - - -## Font styles - -``` -
    -

    - Strong -

    -

    - Emphasis -

    -

    - Inserted -

    -

    - Deleted -

    -

    - StrongEmphasis -

    -

    - Bold -

    -

    - Italic -

    -

    - Citation -

    -

    - Superscript -

    -

    - Subscript -

    -
    -``` - -## Inline code - -``` -
    - - function Y(f) { - var p = function(h) { - return function(x) { - return f(h(h))(x); - }; - }; - return p(p); - } - -
    -``` - -## Preformatted Text - -``` -
    -
    -  This     is      very
    -
    -          formatted
    -              text
    -  
    -
    -``` - -## Unordered List - -``` -
    -
      -
    • Item 1
    • -
    • Item 2 -
        -
      • Subitem 1
      • -
      • - Subitem 2 - -
          -
        • Subsubitem 1
        • -
        • Subsubitem 2
        • -
        • Subsubitem 3
        • -
        -
      • -
      -
    • -
    • Item 3
    • -
    -
    -``` - -## Ordered List - -``` -
    -
      -
    1. Item
    2. -
    3. Item -
        -
      1. Subitem
      2. -
      3. SubItem -
          -
        1. Subsubitem
        2. -
        3. SubsubItem
        4. -
        5. SubsubItem
        6. -
        -
      4. -
      5. SubItem
      6. -
      -
    4. -
    5. Item
    6. -
    -
    -``` - -## Blockquote - -``` -
    -
    -

    - The good news is that you're going to live. The bad news is that he is here to kill you. -

    -
    -
    -``` - -## Link - -``` - -``` - -Links to work packages come in various alternatives: - -* only the ID - -``` -
    -

    #56

    -
    -``` - -* ID with a description - -``` -
    -

    Bug #56 on hold: Work Package link without description 2015-03-27 – 2015-04-30

    -
    -``` - -* ID with description, assignee and responsible and additionally parts of the description - -``` -
    -

    Bug #56 on hold: Work Package link with description 2015-03-27 – 2015-04-30

    -
    Responsible: Ulices Volkman
    Assignee: Danika O'Keefe
    -

    Accedo asporto cicuta cribro canto totam molestias quis. Speculum arma desolo nam volo. Vorago explicabo aut arx. Adficio voluptates qui voluptas. Crur annus consequatur cedo vestrum comminor. Demum sollers bis arcesso dolores agnitio defaeco curso. Copia adversus via appono damno ut territo sed.

    -
    -``` - -## Image - -``` - - -
    -

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Fugit sed cum quam obcaecati eius nisi tenetur tempora odio minus nulla rerum hic, itaque nam dolorum vel fuga quibusdam, praesentium unde!

    - -
    - -
    - -

    Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.

    -
    -``` - -## Table - -``` -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
    Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse
    molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet,
    consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
    Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
    Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer
    adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci
    tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.
    -
    -``` diff --git a/frontend/src/global_styles/content/user-content/_user-content.lsg b/frontend/src/global_styles/content/user-content/_user-content.lsg deleted file mode 100644 index 7cb8e86126d..00000000000 --- a/frontend/src/global_styles/content/user-content/_user-content.lsg +++ /dev/null @@ -1,3 +0,0 @@ -# User Content - -## diff --git a/frontend/src/global_styles/content/work_packages/_table_configuration_modal.sass b/frontend/src/global_styles/content/work_packages/_table_configuration_modal.sass index 70dfd3c85f7..3bf446420c0 100644 --- a/frontend/src/global_styles/content/work_packages/_table_configuration_modal.sass +++ b/frontend/src/global_styles/content/work_packages/_table_configuration_modal.sass @@ -5,6 +5,7 @@ label.option-label float: left margin-right: 20px + font-weight: normal &.-multi-line margin-bottom: 0 diff --git a/frontend/src/global_styles/content/work_packages/_table_content.lsg b/frontend/src/global_styles/content/work_packages/_table_content.lsg deleted file mode 100644 index ef12703a160..00000000000 --- a/frontend/src/global_styles/content/work_packages/_table_content.lsg +++ /dev/null @@ -1,171 +0,0 @@ -# Work packages table - -## with work packages - -``` -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - ID - - - - -
    -
    -
    - - Subject - - - - -
    -
    -
    - - Type - - - - -
    -
    -
    - - Status - - - - -
    -
    -
    - - Priority - - - - -
    -
    -
    - - Assignee - - - - -
    -
    -
    - - - -
    -
    - 1234 - - Lorem ipsum - - User Story -
    -
    - In Progress - - Normal - - John Doe - - - - - - - - - -
    - 1234 - - Lorem ipsum - - User Story -
    -
    - In Progress - - Normal - - John Doe - - - - - - - - - -
    - -
    -
    -``` - -## with no work packages - -``` -
    -
    - - - No work packages to display - -
    -

    Either none have been created or all work packages are filtered out.

    -
    -
    -
    -``` diff --git a/frontend/src/global_styles/content/work_packages/inplace_editing/_display_fields.sass b/frontend/src/global_styles/content/work_packages/inplace_editing/_display_fields.sass index 2b288bcb00b..cf3d47cf0b4 100644 --- a/frontend/src/global_styles/content/work_packages/inplace_editing/_display_fields.sass +++ b/frontend/src/global_styles/content/work_packages/inplace_editing/_display_fields.sass @@ -65,10 +65,14 @@ max-width: 90% display: inline-block vertical-align: middle - line-height: 32px + // Inherit from Parent, e.g. strikethrough for baseline comparison + text-decoration: inherit &:first-of-type padding-right: 5px + .badge + height: 1rem + &.split-time-field white-space: nowrap @@ -110,7 +114,7 @@ overflow: visible .wp-table--cell-container.startDate - padding-left: 24px + padding-left: $work-package--start-date-display-field-padding-left .icon-context position: relative diff --git a/frontend/src/global_styles/content/work_packages/inplace_editing/_edit_fields.sass b/frontend/src/global_styles/content/work_packages/inplace_editing/_edit_fields.sass index c3a3bd9699b..9603bcfba2e 100644 --- a/frontend/src/global_styles/content/work_packages/inplace_editing/_edit_fields.sass +++ b/frontend/src/global_styles/content/work_packages/inplace_editing/_edit_fields.sass @@ -20,7 +20,6 @@ // Full width to inline-edit inputs width: 100% line-height: 24px - font-size: 14px border-radius: 2px input:not([type='checkbox']) diff --git a/frontend/src/global_styles/content/work_packages/inplace_editing/_textareas.sass b/frontend/src/global_styles/content/work_packages/inplace_editing/_textareas.sass index 8ec197c1ad2..f3bf3765f37 100644 --- a/frontend/src/global_styles/content/work_packages/inplace_editing/_textareas.sass +++ b/frontend/src/global_styles/content/work_packages/inplace_editing/_textareas.sass @@ -34,7 +34,7 @@ .inplace-edit--write-value textarea - font-size: 1rem + font-size: var(--body-font-size) line-height: 1.6 .textarea-wrapper diff --git a/frontend/src/global_styles/content/work_packages/tabs/_activities.lsg b/frontend/src/global_styles/content/work_packages/tabs/_activities.lsg deleted file mode 100644 index a05beb038a1..00000000000 --- a/frontend/src/global_styles/content/work_packages/tabs/_activities.lsg +++ /dev/null @@ -1,389 +0,0 @@ -# Work Packages - [Details Pane] - Activities - -``` -
    -
    -

    - March 19, 2014 - - Show activities with comments only - -

    -
    - Avatar - - OpenProject Admin - - - updated on - - 03/19/2014 - 4:38 PM - - -
    - - #1 - -
    - - -
    -
    -
    - - - -
      -
    • - - Custom Integer - changed from - 12442234 - to - 34 - -
    • -
    -
    -
    -
    -
    -

    - April 30, 2014 -

    -
    - Avatar - - OpenProject Admin - - - updated on - - 04/30/2014 - 9:40 AM - - -
    - - #2 - -
    - - -
    -
    -
    - - - -
      -
    • - - Type - changed from - Support - to - Bug - -
    • -
    -
    -
    -
    -
    -

    - July 11, 2014 -

    -
    - Avatar - - OpenProject Admin - - - updated on - - 07/11/2014 - 4:27 PM - - -
    - - #3 - -
    - - -
    -
    -
    - - -

    This is an example wiki text.

    -
    -
    -
    -
    -
    -
    -
    - Avatar - - OpenProject Admin - - - updated on - - 07/11/2014 - 4:27 PM - - -
    - - #4 - -
    - - -
    -
    -
    - - - -
      -
    • - - Description - set (Details) - -
    • -
    -
    -
    -
    -
    -

    - August 21, 2014 -

    -
    - Avatar - - OpenProject Admin - - - updated on - - 08/21/2014 - 11:03 AM - - -
    - - #5 - -
    - - -
    -
    -
    - - - -
      -
    • - - Parent - set to - molestias officia beatae aut et sunt ut labore - -
    • -
    -
    -
    -
    -
    -
    - Avatar - - OpenProject Admin - - - updated on - - 08/21/2014 - 11:05 AM - - -
    - - #6 - -
    - - -
    -
    -
    - - - -
      -
    • - - Parent - deleted ( - molestias officia beatae aut et sunt ut labore - ) - -
    • -
    -
    -
    -
    -
    -

    - September 2, 2014 -

    -
    - Avatar - - OpenProject Admin - - - updated on - - 09/02/2014 - 8:50 AM - - -
    - - #12 - -
    - - -
    -
    -
    - - - -
      -
    • - - Subject - changed from - Nothing important - to - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed felis metus, lobortis eget nisi id, scelerisque sagittis eros. Duis eget tempor risus, id sagittis nisi. In accumsan sapien sed scelerisque mattis. Sed bibendum condimentum magna eget vestibulum. Etiam bibendum, justo vitae efficitur placerat, justo lorem bibendum eros, et mattis diam diam eget urna. Aenean id magna pharetra, auctor dui ut, ultrices turpis. Duis eu massa non libero dictum vestibulum et consectetur diam. Sed varius nisl leo, vitae auctor velit imperdiet in. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse at odio lorem. In at iaculis dui, a convallis nibh. Vivamus rhoncus arcu quis purus euismod rutrum. Morbi a metus vitae metus finibus venenatis. - -
    • -
    -
    -
    -
    -
    -
    - Avatar - - OpenProject Admin - - - updated on - - 09/02/2014 - 8:51 AM - - -
    - - #13 - -
    - - -
    -
    -
    - - - -
      -
    • - - Custom User - set to - Zena Labadie - -
    • -
    • - - Custom Description - changed from - QUAS - to - QUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUAS QUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUASQUAS - -
    • -
    -
    -
    -
    -
    -

    - September 12, 2014 -

    -
    - Avatar - - OpenProject Admin - - - updated on - - 09/12/2014 - 8:49 AM - - -
    - - #14 - -
    - - -
    -
    -
    - - - -
      -
    • - - Description - changed (Details) - -
    • -
    • - - Assignee - set to - Deron Feil - -
    • -
    • - - Responsible - set to - Zena Labadie - -
    • -
    -
    -
    -
    -``` diff --git a/frontend/src/global_styles/content/work_packages/tabs/_relations.sass b/frontend/src/global_styles/content/work_packages/tabs/_relations.sass index cbb100236de..9172463ea65 100644 --- a/frontend/src/global_styles/content/work_packages/tabs/_relations.sass +++ b/frontend/src/global_styles/content/work_packages/tabs/_relations.sass @@ -132,7 +132,7 @@ .wp-relation--description-read-value, .wp-relation--description-textarea min-height: 60px - font-size: 0.875rem + font-size: var(--body-font-size) line-height: var(--base-line-height) padding-top: 10px diff --git a/frontend/src/global_styles/fonts/_index.lsg b/frontend/src/global_styles/fonts/_index.lsg deleted file mode 100644 index 99cc225054f..00000000000 --- a/frontend/src/global_styles/fonts/_index.lsg +++ /dev/null @@ -1,8 +0,0 @@ -# Typography - -Typography is divided into content and UI, depending on where the styles are applied. - -Content refers to user generated information, such as wiki-syntax based entries, e.g. the description of a work package - -UI is everything relating to the application itself. - diff --git a/frontend/src/global_styles/fonts/_lato.lsg b/frontend/src/global_styles/fonts/_lato.lsg deleted file mode 100644 index eb0f71857ed..00000000000 --- a/frontend/src/global_styles/fonts/_lato.lsg +++ /dev/null @@ -1,20 +0,0 @@ -# Fonts - -## Lato - -*Lato* for headlines: - -~~~ -@font-example 32px Lato -ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜ -~~~ - -*Lato* for normal/small text: - -~~~ -@font-example bold 14px Lato -~~~ - -~~~ -@font-example 14px Lato -~~~ diff --git a/frontend/src/global_styles/fonts/_openproject_icon_font.lsg b/frontend/src/global_styles/fonts/_openproject_icon_font.lsg deleted file mode 100644 index 8e538fb35df..00000000000 --- a/frontend/src/global_styles/fonts/_openproject_icon_font.lsg +++ /dev/null @@ -1,304 +0,0 @@ -## OpenProject Icon Font - -*OpenProject Icon Font* for icons: - -
      -
    • accessibility
    • -
    • accountable
    • -
    • activity-history
    • -
    • add-attachment
    • -
    • add-link
    • -
    • add
    • -
    • align-center
    • -
    • align-justify
    • -
    • align-left
    • -
    • align-right
    • -
    • arrow-down1
    • -
    • arrow-down2
    • -
    • arrow-in
    • -
    • arrow-left-right
    • -
    • arrow-left1
    • -
    • arrow-left2
    • -
    • arrow-left3
    • -
    • arrow-left4
    • -
    • arrow-out
    • -
    • arrow-right2
    • -
    • arrow-right3
    • -
    • arrow-right4
    • -
    • arrow-right5
    • -
    • arrow-right6
    • -
    • arrow-right7
    • -
    • arrow-thin
    • -
    • arrow-up1
    • -
    • arrow-up2
    • -
    • assigned-to-me
    • -
    • assigned
    • -
    • attachment
    • -
    • attention
    • -
    • back-up
    • -
    • backlogs
    • -
    • baseline
    • -
    • bcf
    • -
    • bell
    • -
    • billing-information
    • -
    • boards
    • -
    • bold
    • -
    • budget
    • -
    • bug
    • -
    • calendar
    • -
    • calendar2
    • -
    • camera
    • -
    • cancel-circle
    • -
    • cancel
    • -
    • cart
    • -
    • changeset-down
    • -
    • changeset-up
    • -
    • changeset
    • -
    • chart1
    • -
    • chart2
    • -
    • chart3
    • -
    • checkmark-circle
    • -
    • checkmark
    • -
    • clipboard
    • -
    • close
    • -
    • code-tag
    • -
    • color-text
    • -
    • color-underline
    • -
    • column-left
    • -
    • column-right
    • -
    • columns
    • -
    • compare2
    • -
    • concept
    • -
    • console-light
    • -
    • console
    • -
    • contacts
    • -
    • copy
    • -
    • cost-reports
    • -
    • cost-types
    • -
    • cursor
    • -
    • custom-development
    • -
    • custom-fields
    • -
    • cut
    • -
    • date-alert
    • -
    • date-alerts
    • -
    • delete-folder
    • -
    • delete
    • -
    • delta-triangle
    • -
    • dependency
    • -
    • design
    • -
    • double-arrow-left
    • -
    • double-arrow-right
    • -
    • download-arrow
    • -
    • download
    • -
    • drag-handle
    • -
    • dropdown-open
    • -
    • dropdown
    • -
    • duplicate
    • -
    • edit
    • -
    • email-alert
    • -
    • enterprise-addons
    • -
    • enterprise
    • -
    • enumerations
    • -
    • error
    • -
    • export-atom
    • -
    • export-bcf
    • -
    • export-csv
    • -
    • export-pdf-descr
    • -
    • export-pdf-with-descriptions
    • -
    • export-pdf
    • -
    • export-xls-descr
    • -
    • export-xls-with-descriptions
    • -
    • export-xls-with-relations
    • -
    • export-xls
    • -
    • export
    • -
    • external-link
    • -
    • faq
    • -
    • file-doc
    • -
    • file-form
    • -
    • file-presentation
    • -
    • file-sheet
    • -
    • file-text
    • -
    • filter
    • -
    • flag
    • -
    • folder-add
    • -
    • folder-locked
    • -
    • folder-open
    • -
    • folder-remove
    • -
    • folder
    • -
    • forums
    • -
    • from-fullscreen
    • -
    • getting-started
    • -
    • glossar
    • -
    • google-plus
    • -
    • group-by
    • -
    • group
    • -
    • hamburger
    • -
    • headline1
    • -
    • headline2
    • -
    • headline3
    • -
    • headset
    • -
    • help
    • -
    • help1
    • -
    • help2
    • -
    • hierarchy
    • -
    • home
    • -
    • hosting
    • -
    • ifc
    • -
    • image1
    • -
    • image2
    • -
    • import
    • -
    • inbox
    • -
    • info1
    • -
    • info2
    • -
    • input-disabled
    • -
    • installation-services
    • -
    • italic
    • -
    • key
    • -
    • link
    • -
    • loading1
    • -
    • loading2
    • -
    • location
    • -
    • locked
    • -
    • logout
    • -
    • mail1
    • -
    • mail2
    • -
    • maintenance-support
    • -
    • mark-all-read
    • -
    • mark-read
    • -
    • medal
    • -
    • meetings
    • -
    • mention
    • -
    • menu
    • -
    • merge-branch
    • -
    • microphone
    • -
    • milestone
    • -
    • minus1
    • -
    • minus2
    • -
    • mobile
    • -
    • modules
    • -
    • more
    • -
    • move
    • -
    • movie
    • -
    • music
    • -
    • new-planning-element
    • -
    • news
    • -
    • nextcloud-circle
    • -
    • nextcloud
    • -
    • no-hierarchy
    • -
    • no-zen-mode
    • -
    • not-supported
    • -
    • notes
    • -
    • openid
    • -
    • openproject
    • -
    • ordered-list
    • -
    • outline
    • -
    • paragraph-left
    • -
    • paragraph-right
    • -
    • paragraph
    • -
    • payment-history
    • -
    • phone
    • -
    • pin
    • -
    • play
    • -
    • plugins
    • -
    • plus
    • -
    • pre
    • -
    • presentation
    • -
    • preview
    • -
    • print
    • -
    • priority
    • -
    • project-types
    • -
    • projects
    • -
    • publish
    • -
    • pulldown-up
    • -
    • pulldown
    • -
    • quote
    • -
    • quote2
    • -
    • redo
    • -
    • relation-follows
    • -
    • relation-new-child
    • -
    • relation-precedes
    • -
    • relations
    • -
    • reload
    • -
    • reminder
    • -
    • remove-link
    • -
    • remove
    • -
    • rename
    • -
    • reported-by-me
    • -
    • resizer-bottom-right
    • -
    • resizer-vertical-lines
    • -
    • roadmap
    • -
    • rss
    • -
    • rubber
    • -
    • save
    • -
    • search
    • -
    • select-all
    • -
    • send-mail
    • -
    • server-key
    • -
    • settings
    • -
    • settings2
    • -
    • settings3
    • -
    • settings4
    • -
    • shortcuts
    • -
    • show-all-projects
    • -
    • show-more-horizontal
    • -
    • show-more
    • -
    • slack
    • -
    • sort-ascending
    • -
    • sort-by
    • -
    • sort-descending
    • -
    • sort-down
    • -
    • sort-up
    • -
    • square
    • -
    • star
    • -
    • status-reporting
    • -
    • status
    • -
    • strike-through
    • -
    • team-planner
    • -
    • text
    • -
    • ticket-checked
    • -
    • ticket-down
    • -
    • ticket-edit
    • -
    • ticket-minus
    • -
    • ticket-note
    • -
    • ticket
    • -
    • time-tracking-running
    • -
    • time-tracking-start
    • -
    • time-tracking-stop
    • -
    • time
    • -
    • to-fullscreen
    • -
    • training-consulting
    • -
    • two-factor-authentication
    • -
    • types
    • -
    • underline
    • -
    • undo
    • -
    • unit
    • -
    • unlocked
    • -
    • unordered-list
    • -
    • unwatched
    • -
    • upload-arrow
    • -
    • upload
    • -
    • user-minus
    • -
    • user-plus
    • -
    • user
    • -
    • view-card
    • -
    • view-fullscreen
    • -
    • view-list
    • -
    • view-model
    • -
    • view-split-viewer-table
    • -
    • view-split
    • -
    • view-split2
    • -
    • view-timeline
    • -
    • warning
    • -
    • watched
    • -
    • watching
    • -
    • wiki-edit
    • -
    • wiki
    • -
    • wiki2
    • -
    • work-packages
    • -
    • workflow
    • -
    • yes
    • -
    • zen-mode
    • -
    • zoom-auto
    • -
    • zoom-in
    • -
    • zoom-out
    • -
    diff --git a/frontend/src/global_styles/layout/_index.sass b/frontend/src/global_styles/layout/_index.sass index 83a5a6a4891..f038ab076f3 100644 --- a/frontend/src/global_styles/layout/_index.sass +++ b/frontend/src/global_styles/layout/_index.sass @@ -48,3 +48,6 @@ // Print layout @import print + +// View component preview styles +@import viewcomponent_previews diff --git a/frontend/src/global_styles/layout/_main_menu.sass b/frontend/src/global_styles/layout/_main_menu.sass index 5b4ee48c388..d1b95224db1 100644 --- a/frontend/src/global_styles/layout/_main_menu.sass +++ b/frontend/src/global_styles/layout/_main_menu.sass @@ -119,8 +119,10 @@ $arrow-left-width: 40px .toggler width: 40px height: var(--main-menu-item-height) - text-align: center overflow: hidden + display: flex + justify-content: center + align-items: center .icon-time background: none diff --git a/frontend/src/global_styles/layout/_toolbar.lsg b/frontend/src/global_styles/layout/_toolbar.lsg deleted file mode 100644 index daa8f28f23c..00000000000 --- a/frontend/src/global_styles/layout/_toolbar.lsg +++ /dev/null @@ -1,143 +0,0 @@ -# Toolbar - -A toolbar that can and should be used for actions on the current view. Initially designed for the Work package list, this can be reused throughout the application. - -## Standard Button Bar - -``` -@full-width -
    -
    -
    -

    Title of the page

    -
    - -
    -
    -``` - -## Toolbar with form elements - -``` -@full-width -
    -
    -
    -

    Dragonball Z characters

    -
    -
      -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - - - Add - -
    • -
    -
    -

    now with extremely long subtitle: Lorem ipsum dolor sit amet, consectetur adipisicing elit. Iste consequatur doloribus suscipit nemo temporibus deserunt alias incidunt doloremque officia rerum, nobis fuga, recusandae voluptatibus voluptatem tenetur repellendus itaque et. Eum.

    -
    -``` - -## Toolbar with affix form elements - -``` -@full-width -
    -
    -
    -

    Checkout information

    -
    -
      -
    • -
      - git clone -
      - -
    • -
    • - -
      - Read + Write -
      -
    • -
    -
    -
    -``` - -## Toolbar with labelled form elements - -``` -@full-width -
    -
    -
    -

    Dragonball Z characters

    -
    -
      -
    • -
      - -
      - -
    • -
    • -
      - -
      - -
    • -
    -
    -

    now with extremely long subtitle: Lorem ipsum dolor sit amet, consectetur adipisicing elit. Iste consequatur doloribus suscipit nemo temporibus deserunt alias incidunt doloremque officia rerum, nobis fuga, recusandae voluptatibus voluptatem tenetur repellendus itaque et. Eum.

    -
    -``` diff --git a/frontend/src/global_styles/layout/_toolbar.sass b/frontend/src/global_styles/layout/_toolbar.sass index 4153b3fb1d2..bcc48fb8ecb 100644 --- a/frontend/src/global_styles/layout/_toolbar.sass +++ b/frontend/src/global_styles/layout/_toolbar.sass @@ -104,6 +104,9 @@ $nm-color-success-background: #d8fdd1 &.-no-grow flex-grow: 0 + a.button + text-decoration: none + .button overflow: hidden white-space: normal diff --git a/frontend/src/global_styles/layout/_viewcomponent_previews.sass b/frontend/src/global_styles/layout/_viewcomponent_previews.sass new file mode 100644 index 00000000000..a403a62ca0c --- /dev/null +++ b/frontend/src/global_styles/layout/_viewcomponent_previews.sass @@ -0,0 +1,3 @@ +.viewcomponent-preview + &--content + margin: 1rem diff --git a/frontend/src/global_styles/layout/work_packages/_table_baseline.sass b/frontend/src/global_styles/layout/work_packages/_table_baseline.sass index 7cae601a483..531ee950b64 100644 --- a/frontend/src/global_styles/layout/work_packages/_table_baseline.sass +++ b/frontend/src/global_styles/layout/work_packages/_table_baseline.sass @@ -56,6 +56,9 @@ // Ensure items are not taking full width align-items: flex-start + &.startDate + padding-left: $work-package--start-date-display-field-padding-left + &--column-header display: flex align-items: center diff --git a/frontend/src/global_styles/openproject/_forms.sass b/frontend/src/global_styles/openproject/_forms.sass index 92be19c4199..578aafbb594 100644 --- a/frontend/src/global_styles/openproject/_forms.sass +++ b/frontend/src/global_styles/openproject/_forms.sass @@ -26,7 +26,6 @@ // See COPYRIGHT and LICENSE files for more details. //++ -@import "variables" /* FORMS (taken from Foundation for Apps) @@ -43,7 +42,7 @@ - Progress bars and meters // Basic form variables -$form-fontsize: 1rem !default +$form-fontsize: 0.875rem !default $form-padding: 0.5rem !default // Text fields diff --git a/frontend/src/global_styles/openproject/_homescreen.sass b/frontend/src/global_styles/openproject/_homescreen.sass index 46b8d0394e8..6fa8711f8d3 100644 --- a/frontend/src/global_styles/openproject/_homescreen.sass +++ b/frontend/src/global_styles/openproject/_homescreen.sass @@ -27,7 +27,6 @@ //++ @import mixins -@import 'variables' .controller-homescreen #content-wrapper .widget-box diff --git a/frontend/src/global_styles/openproject/_mixins.sass b/frontend/src/global_styles/openproject/_mixins.sass index 2c3287dc8fd..c802dba803d 100644 --- a/frontend/src/global_styles/openproject/_mixins.sass +++ b/frontend/src/global_styles/openproject/_mixins.sass @@ -300,7 +300,7 @@ $scrollbar-size: 10px @mixin board-header-editable-toolbar-title - line-height: 1 !important + line-height: normal !important @mixin global-breadcrumb-styles margin-top: 10px diff --git a/frontend/src/global_styles/openproject/_variable_defaults.scss b/frontend/src/global_styles/openproject/_variable_defaults.scss index 3e84485562a..e36b7df0fe6 100644 --- a/frontend/src/global_styles/openproject/_variable_defaults.scss +++ b/frontend/src/global_styles/openproject/_variable_defaults.scss @@ -112,7 +112,7 @@ --drop-down-hover-bg-color: #EFEFEF; --context-menu-unselected-font-color: var(--body-font-color); --context-menu-hover-font-color: var(--drop-down-selected-font-color); - --wiki-default-font-size: 1rem; + --wiki-default-font-size: var(--body-font-size); --user-avatar-border-radius: 50%; --inplace-edit--border-color: #ddd; --inplace-edit--dark-background: var(--gray-light); @@ -142,7 +142,6 @@ --timeline--type-fallback-color: rgba(150, 150, 150, 0.8); --table-timeline--row-height: 40px; --status-selector-bg-color: #F99601; - --card-font-size: 16px; --warn:#C92A2A; --grid-background-color: #F3F6F8; } diff --git a/frontend/src/global_styles/openproject/_variables.sass b/frontend/src/global_styles/openproject/_variables.sass index 70fa820e693..f04be007cc7 100644 --- a/frontend/src/global_styles/openproject/_variables.sass +++ b/frontend/src/global_styles/openproject/_variables.sass @@ -1,11 +1,12 @@ // A selector that checks whether we are running in a test environment $spec-active-selector: '.env-test' +$work-package--start-date-display-field-padding-left: 1.5rem // All different input types and textarea $text-input-selectors: 'input[type="text"], input[type="password"], input[type="date"], input[type="datetime"], input[type="datetime-local"], input[type="month"], input[type="week"], input[type="email"], input[type="number"], input[type="search"], input[type="tel"], input[type="time"], input[type="url"], input[type="color"], textarea' // A global used variable for all labels -$form-label-fontsize: 0.9rem !default +$form-label-fontsize: var(--body-font-size) !default // These breakpoint widths ( -1) were taken from Primer // The substraction was needed since they have a "min-width" approach for their breakpoints and we have a "max-width" diff --git a/frontend/src/spot.scss b/frontend/src/spot.scss new file mode 100644 index 00000000000..d20d5913082 --- /dev/null +++ b/frontend/src/spot.scss @@ -0,0 +1,9 @@ +// SPOT variables +@import "src/assets/sass/_helpers.sass"; + +// Fonts +@import "src/global_styles/fonts/_index.sass"; + +// SPOT styles +@import "app/spot/styles/sass/common"; +@import "app/spot/styles/sass/components"; diff --git a/frontend/src/stimulus/controllers/dynamic/admin/custom-fields.controller.ts b/frontend/src/stimulus/controllers/dynamic/admin/custom-fields.controller.ts index fbb4d74b139..22c0e1208f7 100644 --- a/frontend/src/stimulus/controllers/dynamic/admin/custom-fields.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/admin/custom-fields.controller.ts @@ -311,6 +311,7 @@ export default class CustomFieldsController extends Controller { this.unsearchable(); break; case 'version': + this.show(...this.multiSelectTargets); this.activate(this.defaultValueTargets, false); this.activate(this.possibleValuesTargets, false); this.hide(...this.lengthTargets, ...this.regexpTargets, ...this.defaultValueTargets); diff --git a/frontend/src/stimulus/controllers/dynamic/project-storage-form.controller.ts b/frontend/src/stimulus/controllers/dynamic/project-storage-form.controller.ts index fbbd2735169..09cde52c2de 100644 --- a/frontend/src/stimulus/controllers/dynamic/project-storage-form.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/project-storage-form.controller.ts @@ -157,6 +157,8 @@ export default class ProjectStorageFormController extends Controller { this.projectFolderIdInputTarget.value = ''; } + this.folderModeValue = mode; + this.toggleFolderDisplay(mode); } diff --git a/frontend/src/stories/DatePicker.mdx b/frontend/src/stories/DatePicker.mdx deleted file mode 100644 index efda10b64c2..00000000000 --- a/frontend/src/stories/DatePicker.mdx +++ /dev/null @@ -1,77 +0,0 @@ -import { Canvas, Meta, ArgsTable } from '@storybook/blocks'; - -import { OpBasicSingleDatePickerComponent } from '../app/shared/components/datepicker/basic-single-date-picker/basic-single-date-picker.component' -import { OpBasicRangeDatePickerComponent } from '../app/shared/components/datepicker/basic-range-date-picker/basic-range-date-picker.component' - -import * as DatePickerStories from './DatePicker.stories'; - - - -# Basic Date Picker - -The basic date picker is a key element in OpenProject and is displayed any time the user has to input a date. - -Basic date pickers are attached to existing date input fields and is displayed as a drop-down when that date input field is in focus. It consists of only the mini-calendar component. - -The basic date picker can also be placed inside modals like the work package date drop modal and the Baseline drop modal. - - - -## External dependencies - -All date picker are built on the [Flatpickr javascript library](https://flatpickr.js.org/). The library gives us certain functionality out of the box with a fairly high degree of customisation, but also introduces limits (which will be mentioned below when relevant). - -Please read the [Flatpickr documentation](https://flatpickr.js.org/instance-methods-properties-elements/) before using or contributing to date pickers. - -## Sub-variants - -The structure is very basic and consists only of one mini calendar. There are two sub-variants: - -### Single - -**Single** allows picking just one date (2023-02-09) - - - -### Range - -**Range:** allows inputing a range (2023-02-09 - 2023-02-14), and shows the date picker with two months. - - - -## Mobile - -The basic date picker does not have a mobile version. On mobile devices, Flatpickr will automatically gracefully degrade to the device's native date picker. - -For more complex implementations involving date pickers (the work package date field or the baseline modal), please refer to documentation concerning those specific features for notes on mobile-specific rendering. - -## Accessibility - -The basic date picker itself never receives focus (and thus does not afford keyboard navigation or screen reader compatibility). However, the date input field that the date picker is attached to can receive focus and all users can type in the desired date in the ISO format. - -## Usage - -The basic date picker is used in: - -### Single date picker - -- Log time -- Log unit costs -- Custom fields (work package and project) -- Meeting minutes -- Work package table filters -- Work package bulk edit form -- Announcement edit form -- Version new and edit form and custom fields -- Project filters -- Cost report filters -- Budget forms -- Dynamic forms - - - -### Range date picker - -- Pause date reminders - - diff --git a/frontend/src/stories/DatePicker.stories.ts b/frontend/src/stories/DatePicker.stories.ts deleted file mode 100644 index 23291623ee7..00000000000 --- a/frontend/src/stories/DatePicker.stories.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/angular'; -import { moduleMetadata } from '@storybook/angular'; -import * as moment from 'moment'; - -import { TimezoneService } from '../app/core/datetime/timezone.service'; -import { TimezoneServiceStub } from './timezone.service.stub'; - -import { I18nService } from '../app/core/i18n/i18n.service'; -import { I18nServiceStub } from './i18n.service.stub'; - -import { WeekdayService } from '../app/core/days/weekday.service'; -import { WeekdayServiceStub } from './weekday.service.stub'; - -import { DayResourceService } from '../app/core/state/days/day.service'; -import { DayResourceServiceStub } from './day.service.stub'; - -import { ConfigurationService } from '../app/core/config/configuration.service'; -import { ConfigurationServiceStub } from './configuration.service.stub'; - -import { States } from '../app/core/states/states.service'; - -import { OpBasicDatePickerModule } from '../app/shared/components/datepicker/basic-datepicker.module'; -import { OpBasicSingleDatePickerComponent } from '../app/shared/components/datepicker/basic-single-date-picker/basic-single-date-picker.component' - -const meta:Meta = { - title: 'Patterns/DatePicker', - component: OpBasicSingleDatePickerComponent, - decorators: [ - moduleMetadata({ - imports: [ - OpBasicDatePickerModule, - ], - providers: [ - { - provide: TimezoneService, - useValue: TimezoneServiceStub, - }, - { - provide: ConfigurationService, - useValue: ConfigurationServiceStub, - }, - { - provide: States, - useValue: new States(), - }, - { - provide: I18nService, - useValue: I18nServiceStub, - }, - { - provide: WeekdayService, - useFactory: () => new WeekdayServiceStub(), - }, - { - provide: DayResourceService, - useFactory: () => new DayResourceServiceStub(), - }, - ], - }), - ], -}; - -export default meta; -type Story = StoryObj; - -export const Single:Story = { - render: (args) => ({ - props: { - ...args, - }, - template: ` - - `, - }), -}; - -export const SingleWithValue:Story = { - render: (args) => ({ - props: { - ...args, - date: moment(new Date()).format('YYYY-MM-DD'), - }, - template: ` - - `, - }), -}; - -export const RangeWithValue:Story = { - render: (args) => ({ - props: { - ...args, - dates: [ - moment(new Date()).format('YYYY-MM-DD'), - moment(new Date()).add(4, 'days').format('YYYY-MM-DD'), - ], - }, - template: ` - - `, - }), -}; diff --git a/frontend/src/stories/HowToUse.mdx b/frontend/src/stories/HowToUse.mdx deleted file mode 100644 index 8bf60226680..00000000000 --- a/frontend/src/stories/HowToUse.mdx +++ /dev/null @@ -1,67 +0,0 @@ -import { Meta } from '@storybook/addon-docs'; - - - -# Using this resource - -We use [Storybook](https://storybook.js.org/) to document our design system. You will find defined styles, components and patterns in the relevant sections. - -You will not find _all_ elements in Storybook yet. The design system at OpenProject is a work in progress; many parts of the software still use older elements that have not been standardised and defined. - -As we work through defining and coumenting each new component, we will find them here. - -## Other resources - -All of the components described here also exist in our Figma libraries: - -- [Foundation styles](https://www.figma.com/file/vOw6PEVIyzaQOIgf02VZFW/Foundations-Library?node-id=228%3A2) -- [Components](https://www.figma.com/file/XhCsrvs6rePifqbBpKYRWD/Components-Library?node-id=386%3A3606) - -_You might find examples of some older elements in the older [Living Style Guide](http://spike.openproject-stage.com:4200/assets/styleguide.html#forms). This will eventually be replaced by Storybook, but this will require time._ - - -## Semantics - -When describing components, we use certain words in very particular ways: - -### Disabled - -_Disabled_ is when no interaction is possible. A disabled element can itself have multiple states: a checkbox can be checked and disabled, a switch can be ON and disabled, a dropdown might have a value but but disabled. - -The opposite of disabled is _enabled_. All elements are described in their enabled state unless explicitly mentioned. - -### Focused and active - -Focus simply refers to when an element has the current object in DOM that has the input focus. In OpenProject, elements that are focused generally have a blue outline. - -The opposite of _in focus_ (or focused) is _not in focus_ (or focused). - -_Active_ is a similar state as focused, but not entirely the same. For example, in the date picker, the start date can be "active" (meaning that clicking on a date in the mini-calendar will change the start date) but not in focus (because the mouse has clicked outside of the field). - -The opposite of active is _inactive_. - -### Checked and on - -Checkboxes can be _checked_ (true) or _unchecked_ (false) - -Radio options can also be "checked" (as in, in a set of three options, only one can be checked) despite this not being the technically correct word. This isi to to distinguish it from "selected" (which refers to selecting an element or an area with the cursor). - -Switches can be _ON_ (true) or _OFF_ (false). - -## Browsing through the docs - -This Storybook is divided in three sections: - -**Styles** include foundational elements like typography, colour, spacing and shadows. All other elements are defined using these base styles. - -**Components** are individual elements (like checkboxes, buttons or text fields), each of which has its own states, variants and behaviour. - -**Patterns** are larger, recurring UI elements made up of a set of different components. Each component used in a pattern still retains its own specificities (options, variatns), but can also have certiain behaviour at the level of the pattern itself. (For example, an action bar can have two or three buttons, which can either be on the left side or the right side). - -## To be added: - -- What the left-side menu includes (and does not include) -- How to find linked/dependent elements -- Contributing: - - As a developer - - As a designer diff --git a/frontend/src/stories/Introduction.mdx b/frontend/src/stories/Introduction.mdx index 005628309fc..6e9ac0e5ab1 100644 --- a/frontend/src/stories/Introduction.mdx +++ b/frontend/src/stories/Introduction.mdx @@ -1,64 +1,8 @@ import { Meta } from '@storybook/addon-docs'; - + -# OpenProject Design System +# OpenProject Angular SPOT components -At OpenProject, we use a design system to help ensure that our design delivers a consistent experience to users. The system describes the styles, components and patterns that come together to build the foundation for a consistent user experience. To do this, we provide documentation explaining how each element should be used, the different states, variations and options it offers, along with code examples. - -Our goal is to reach a point where every view in OpenProject is built with our design system principles and patterns in mind. However, we are aware that reaching this goal will take time for a tool as complex and layered as OpenProject. - -As such, the design system is still in its infancy and will grow with each release. - -## Purpose (what it is and what it's not) - -This Storybook-based design system documentation only seeks to describe the actual reusable components; it does not seek to document features where these components are used. - -**The purpose of the design system is to:** - -- Ensure that OpenProject has a beautiful, usable and delightful design -- Improve consistency and predictability by defining standards that are reused or extended when there is a need for deviations or alternate versions -- Improve quality (a component that is reused is likely to have gone through more testing and adjustment with each reuse and testing cycle) -- Make design assets easier to maintain (change once to change all) -- Improve accessibility by taking accessibility requirements into account when designing components and patterns - -**The purpose of this document is to:** - -- Maintain a singular reference on what reusable components and patterns look like, how they behave and what options they provide -- Allow product designers and front-end developers to have a shared understanding of how these common elements are designed and how/when they should be used -- Provide quality assurance personnel a common base of reference when testing features that use these reusable components and patterns (and there is a doubt about an unnecessary deviation) -- Give developers important context about available components and how to use them. We want to enable both team and community members to compose and develop complex interfaces easily, preventing fragmentation and technical debt where possible. - -**This document does not:** - -- Define every single component in OpenProject -- Define specific features or implementation details -- Define how things *should* look like; the stories contained here will only describe things as they are currently implemented -- Define possible exceptions or variations made within a feature (if necessary) -- Seek to serve as a central documentation for QA testing of features -- Seek to be exhaustive at any point; it will remain a living document that require maintenance as changes are made - - -## SPOT - -Our design system is referred to simply as "OpenProject Design System". - -You will often see the `spot-` prefix used in code; this is primarily to distinguish newer refactored components from older elements that have the `op-` prefix. - -## Approach - -OpenProject is a complex, powerful tool. One of its key strengths is its customisability and its ability to adapt to a range of different needs. This includes complex filtering options, custom types and statuses, custom fields and a wide range of options to configure views and work package forms. - -Nevertheless, it is very important that OpenProject be intuitive for new users who might not necessarily need that complexity, or indeed be overwhelmed by it. - -Our design approach aims to strike the right balance between powerful and accessible with a two-tiered approach: apply sane defaults and present the most common options, and allow advanced users the option (via an additional click) to customise and fine-tune. - -## The UX of Open Source - -As an open source project with a considerably long history and a large number of contributors, different parts of OpenProject have evolved at different paces, sometimes with completely different technology. Similar components are sometimes implemented somewhat differently in different parts of the software, and there are even multiple implementations of the same basic design. - -This is quite normal for a large open-source project that has not had a dedicated design team for most of its conception. - -One of the goals of the design system is to introduce more coherence and introduce a more modern design language. Whilst we would naturally prefer to be able to update everything at the same time and push the new design system to the entire software, we recognise the need for a more pragmatic approach. The design system will be rolled out in phases, with a careful study of the consequences of updating each component or pattern, and the potential dependencies that will be affected. - -_We recognise that UI/UX has not always been the highest priority for open-source projects. This is somewhat understandable given how open source projects have relatively fewer design resources dedicated to it than commercial products. Our goal is to do our part to improve that situation as much as we can and document our process._ +This page contains documentation for OpenProject angular components and is mostly deprecated. +Documentation will be kept updated instead in Lookbok, which you can find in a development server under http://localhost:3000/lookbook diff --git a/frontend/src/stories/Mobile-Accessibility-Localisation.mdx b/frontend/src/stories/Mobile-Accessibility-Localisation.mdx deleted file mode 100644 index c9474612ca5..00000000000 --- a/frontend/src/stories/Mobile-Accessibility-Localisation.mdx +++ /dev/null @@ -1,27 +0,0 @@ -import { Meta } from '@storybook/blocks'; - - - -# Devices and Accessibility - -## Desktop-first - -OpenProject is not primarily designed for mobile use or with a mobile-first approach, despite most parts of the software adapting fairly well to smaller screens. The majority of features and views are designed to take advantage of larger screens and more complex interactions, including keyboard shortcuts. - -Nevertheless,  certain features are particularly useful in a mobile context. These include accessing notifications on the go, viewing work packages, reading and responding to comments and viewing attachments and linked files. These features will be given particular attention and optimised for mobile use. - -We do not consider project planning, complex scheduling and team planning to be priority use cases on mobile. - -## Accessibility - -We recognise the importance of accessibility and are aware that OpenProject still has a fair bit of progress to make in this regard. We intend to initially focus on improving contrast, ensuring UI elements have meaningful alt-text and descriptions and expanding the support for keyboard shortcuts. - -We will incrementally evaluate ways to improve our design to better serve users with visual, auditory or motor impairment. - -## Localisation and internationalisation - -OpenProject supports several languages. Our designs must take internalisation into account, notably in terms of string length and spacing. - -The primary design language is English. Our Figma prototypes are always designed first with strings in English and, when relevant, tested with German, French and Spanish translations. This covers our largest user base. - -For other languages, we rely on our translators and our community on \[Crowdin\](https://crowdin.com/translate/openproject/) for their help. If there are design-specific issues that are present in certain languages, we encourage the community to file bug reports so we may fix them. diff --git a/frontend/src/stories/Spacings.mdx b/frontend/src/stories/Spacings.mdx deleted file mode 100644 index 5e36e7adacc..00000000000 --- a/frontend/src/stories/Spacings.mdx +++ /dev/null @@ -1,69 +0,0 @@ -import { Meta } from '@storybook/blocks'; -import tokens from '../app/spot/styles/tokens/dist/tokens.json'; - - - -# Spacings - -If I write some explanatory text around these spacings. - -
    - {Object.keys(tokens) - .filter(key => key.startsWith('spot-spacing-')) - .map(key => ({ - i: parseFloat(key.split('-')[2].replace('_', '.'), 10), - name: key, - })) - .sort((a, b) => a.i - b.i) - .map(({ name }) => ( -
    -
    ${name}
    -
    {tokens[name]}
    -
    -
    - ))} -
    - - - diff --git a/frontend/src/stories/day.service.stub.ts b/frontend/src/stories/day.service.stub.ts deleted file mode 100644 index cfcae302fa0..00000000000 --- a/frontend/src/stories/day.service.stub.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Observable, of } from 'rxjs'; -import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type'; -import { - ApiV3ListParameters, -} from 'core-app/core/apiv3/paths/apiv3-list-resource.interface'; -import { DayStore } from 'core-app/core/state/days/day.store'; -import { IDay } from 'core-app/core/state/days/day.model'; -import { ResourceStore } from 'core-app/core/state/resource-store.service'; - -@Injectable() -export class DayResourceServiceStub { - protected basePath():string { - return ''; - } - - async isNonWorkingDay$(input:Date):Promise { - const day = input.getDay(); - return day > 5 || day === 0; - } - - requireNonWorkingYear$(_date:Date|string):Observable { - return of([]); - } - - requireNonWorkingYears$(_start:Date|string, _end:Date|string):Observable { - return of([]); - } - - fetchCollection(_params:ApiV3ListParameters):Observable> { - return of({ - _type: 'Collection', - count: 0, - pageSize: 10, - offset: 0, - total: 0, - _embedded: { - elements: [], - }, - }); - } - - protected createStore():ResourceStore { - return new DayStore(); - } -} diff --git a/frontend/src/stories/shadows-data.jsx b/frontend/src/stories/shadows-data.jsx deleted file mode 100644 index 77eb288be86..00000000000 --- a/frontend/src/stories/shadows-data.jsx +++ /dev/null @@ -1,2 +0,0 @@ -export const cols = ['Low', 'Mid', 'High']; -export const rows = ['Light', 'Hard']; diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 28af75e6432..e62fb30b80c 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -6,5 +6,7 @@ // Variables @import "global_styles/openproject/_variable_defaults.scss"; +@import "global_styles/openproject/_variables.sass"; + // Core styles @import "global_styles/openproject.sass"; diff --git a/frontend/src/test/i18n-shim.ts b/frontend/src/test/i18n-shim.ts index b29383beea9..c1f954ba170 100644 --- a/frontend/src/test/i18n-shim.ts +++ b/frontend/src/test/i18n-shim.ts @@ -1,7 +1,6 @@ -import { GlobalI18n } from 'core-app/core/i18n/i18n.service'; import * as _ from 'lodash'; -export class I18nShim implements GlobalI18n { +export class I18nShim { public defaultLocale = 'en'; public firstDayOfWeek = 1; @@ -12,8 +11,6 @@ export class I18nShim implements GlobalI18n { en: {}, }; - public pluralization = {}; - t(key:string):T { return `[missing "${key}" translation]` as unknown as T; } diff --git a/lib/api/v3/work_packages/eager_loading/checksum.rb b/lib/api/v3/work_packages/eager_loading/checksum.rb index b71233de468..98bffa43a61 100644 --- a/lib/api/v3/work_packages/eager_loading/checksum.rb +++ b/lib/api/v3/work_packages/eager_loading/checksum.rb @@ -67,7 +67,7 @@ module API end def checksum_associations - %i[status author responsible assigned_to version priority category type] + %i[status author responsible assigned_to version priority category type budget] end def md5_checksum_table_name(association_name) diff --git a/lib/api/v3/work_packages/eager_loading/custom_value.rb b/lib/api/v3/work_packages/eager_loading/custom_value.rb index 40e2a888957..5fbd55b980f 100644 --- a/lib/api/v3/work_packages/eager_loading/custom_value.rb +++ b/lib/api/v3/work_packages/eager_loading/custom_value.rb @@ -31,14 +31,15 @@ module API module WorkPackages module EagerLoading class CustomValue < Base + def initialize(work_packages, **options) + super + + WorkPackage.preload_available_custom_fields(work_packages) + end + def apply(work_package) load_custom_values(work_package) load_custom_values_values(work_package) - load_available_custom_fields(work_package) - end - - def self.module - ::API::V3::Utilities::EagerLoading::CustomFieldAccessor end private @@ -65,10 +66,6 @@ module API end end - def load_available_custom_fields(work_package) - work_package.available_custom_fields = custom_fields_of(work_package).to_a - end - def grouped_custom_values @grouped_custom_values ||= begin custom_values = ::CustomValue @@ -110,7 +107,7 @@ module API def eager_load_values(field_format, scope) cvs = custom_values_of(field_format) - ids_of_values = cvs.map(&:value).select { |v| v =~ /\A\d+\z/ } + ids_of_values = cvs.map(&:value).grep(/\A\d+\z/) return {} if ids_of_values.empty? @@ -126,67 +123,11 @@ module API .select { |cv| cv.custom_field && cv.custom_field.field_format == field_format && cv.value.present? } end - def usages - @usages ||= ActiveRecord::Base - .connection - .select_all(configured_fields_sql) - .to_a - .uniq - end - def custom_field(id) - @loaded_custom_fields_by_id ||= WorkPackageCustomField - .where(id: usages.map { |u| u['custom_field_id'] }.uniq) - .index_by(&:id) + @loaded_custom_fields_by_id ||= work_packages.map(&:available_custom_fields).flatten.uniq.index_by(&:id) @loaded_custom_fields_by_id[id] end - - def usage_map - @usage_map ||= usages.inject(usage_hash) do |hash, by| - cf = custom_field(by['custom_field_id']) - target_project_id = by['project_id'] - - # If the project_id is NOT nil, and the custom_field is `is_for_all` - # Ensure that it gets added to hash[nil] (Regression #28435) - if by['project_id'].present? && cf.is_for_all - target_project_id = nil - end - - hash[target_project_id][by['type_id']] << cf - - hash - end - end - - def custom_fields_of(work_package) - usage_map[work_package.project_id][work_package.type_id] + - usage_map[nil][work_package.type_id] - end - - def configured_fields_sql - WorkPackageCustomField - .left_joins(:projects, :types) - .where(projects: { id: work_packages.map(&:project_id).uniq }, - types: { id: work_packages.map(&:type_id).uniq }) - .or(WorkPackageCustomField - .left_joins(:projects, :types) - .references(:projects, :types) - .where(is_for_all: true)) - .select('projects.id project_id', - 'types.id type_id', - 'custom_fields.id custom_field_id') - .to_sql - end - - def usage_hash - Hash.new do |by_project_hash, project_id| - by_project_hash[project_id] = Hash.new do |by_type_hash, type_id| - # Use a set to ensure CFs are only available once - by_type_hash[type_id] = Set.new - end - end - end end end end diff --git a/lib/api/v3/work_packages/eager_loading/historic_attributes.rb b/lib/api/v3/work_packages/eager_loading/historic_attributes.rb index 9f1df3efb2a..57aaf4d47cd 100644 --- a/lib/api/v3/work_packages/eager_loading/historic_attributes.rb +++ b/lib/api/v3/work_packages/eager_loading/historic_attributes.rb @@ -158,15 +158,5 @@ module API::V3::WorkPackages::EagerLoading def timestamp new_record? ? @timestamp : Timestamp.parse(__getobj__.timestamp) end - - def available_custom_fields - # The Journable::WithHistoricAttributes#load_custom_values loads the historic - # Journal::CustomizableJournal objects and they are being dubbed as custom values. - # The custom_values contain only the historic Journal::CustomizableJournal objects - # that were available on the WorkPackage at the timestamp we are looking at. - # The intersection is made with the super to filter out any historic custom fields that - # are not activated for the work package. - @available_custom_fields ||= super.intersection(custom_values.map(&:custom_field)) - end end end diff --git a/lib/api/v3/work_packages/show_end_point.rb b/lib/api/v3/work_packages/show_end_point.rb index 261b9258ce9..5e252800fae 100644 --- a/lib/api/v3/work_packages/show_end_point.rb +++ b/lib/api/v3/work_packages/show_end_point.rb @@ -29,11 +29,21 @@ module API::V3::WorkPackages class ShowEndPoint < API::V3::Utilities::Endpoints::Show def render(request) + timestamps = Timestamp.parse_multiple(request.params[:timestamps]) + forbidden_timestamps = timestamps - Timestamp.allowed(timestamps) + + if forbidden_timestamps.any? + message = + I18n.t(:'activerecord.errors.models.query.attributes.timestamps.forbidden', + values: forbidden_timestamps.join(", ")) + raise ::API::Errors::BadRequest.new(message) + end + API::V3::WorkPackages::WorkPackageRepresenter .create(request.instance_exec(request.params, &instance_generator), current_user: request.current_user, embed_links: true, - timestamps: Timestamp.parse_multiple(request.params[:timestamps])) + timestamps:) end end end diff --git a/lib/api/v3/work_packages/work_package_at_timestamp_representer.rb b/lib/api/v3/work_packages/work_package_at_timestamp_representer.rb index fdd48083ada..e5cc91ce934 100644 --- a/lib/api/v3/work_packages/work_package_at_timestamp_representer.rb +++ b/lib/api/v3/work_packages/work_package_at_timestamp_representer.rb @@ -78,7 +78,7 @@ module API end def compile_links_for(configs, *args) - super(configs.select { |config| rendered_properties.include?(config.first[:rel]) }, + super(configs.select { |config| rendered_properties_for_links.include?(config.first[:rel]) }, *args) end @@ -94,6 +94,21 @@ module API end end + # This separate property list is a workaround and ideally it is not required. + # The reason is that names in the representable_map are underscored "custom_fields_1", + # the :rel names in the config from the compile_links_for method are lower camel-cased "customField1". + # The rendered_properties method contains the underscored names and the rendered_properties_for_links + # contains the lower camel-cased names. + def rendered_properties_for_links + @rendered_properties_for_links ||= rendered_properties.map do |property| + if property.starts_with?("custom_field_") + API::Utilities::PropertyNameConverter.from_ar_name(property) + else + property + end + end + end + def changed_properties_as_api_name # This conversion is good enough for the set of supported properties as it # * Converts assigned_to_id to assignee diff --git a/lib/api/v3/work_packages/work_package_collection_representer.rb b/lib/api/v3/work_packages/work_package_collection_representer.rb index 8133ba9fc4c..47307208cab 100644 --- a/lib/api/v3/work_packages/work_package_collection_representer.rb +++ b/lib/api/v3/work_packages/work_package_collection_representer.rb @@ -185,10 +185,6 @@ module API def schemas schemas = schema_pairs.map do |project, type, available_custom_fields| - # This hack preloads the custom fields for a project so that they do not have to be - # loaded again later on - project.instance_variable_set(:@all_work_package_custom_fields, all_cfs_of_project[project.id]) - Schema::TypedWorkPackageSchema.new(project:, type:, custom_fields: available_custom_fields) end @@ -221,12 +217,6 @@ module API end end - def all_cfs_of_project - @all_cfs_of_project ||= represented - .group_by(&:project_id) - .transform_values { |wps| wps.map(&:available_custom_fields).flatten.uniq } - end - def paged_models(models) super.pluck(:id) end diff --git a/lib/api/v3/work_packages/work_package_eager_loading_wrapper.rb b/lib/api/v3/work_packages/work_package_eager_loading_wrapper.rb index e29ca66c356..7207bc0ed02 100644 --- a/lib/api/v3/work_packages/work_package_eager_loading_wrapper.rb +++ b/lib/api/v3/work_packages/work_package_eager_loading_wrapper.rb @@ -77,8 +77,11 @@ module API ::API::V3::WorkPackages::EagerLoading::Project, ::API::V3::WorkPackages::EagerLoading::Checksum, ::API::V3::WorkPackages::EagerLoading::CustomValue, - ::API::V3::WorkPackages::EagerLoading::HistoricAttributes, - ::API::V3::WorkPackages::EagerLoading::CustomAction + ::API::V3::WorkPackages::EagerLoading::CustomAction, + # Have the historic attributes last as they require the custom values + # to be loaded first in order to create the diffs between the current + # and the historic values without loading the custom fields (JournableDiffer). + ::API::V3::WorkPackages::EagerLoading::HistoricAttributes ] end diff --git a/lib/open_project/events.rb b/lib/open_project/events.rb index 0437b543a4c..a93f29f678a 100644 --- a/lib/open_project/events.rb +++ b/lib/open_project/events.rb @@ -48,8 +48,9 @@ module OpenProject MEMBER_CREATED = 'member_created'.freeze MEMBER_UPDATED = 'member_updated'.freeze - # Called like this for historic reasons, should be called 'member_destroyed' - MEMBER_DESTROYED = 'member_removed'.freeze + MEMBER_DESTROYED = 'member_destroyed'.freeze + + OAUTH_CLIENT_TOKEN_CREATED = 'oauth_client_token_created'.freeze TIME_ENTRY_CREATED = "time_entry_created".freeze @@ -59,7 +60,14 @@ module OpenProject PROJECT_UPDATED = "project_updated".freeze PROJECT_RENAMED = "project_renamed".freeze + PROJECT_STORAGE_CREATED = "project_storage_created".freeze + PROJECT_STORAGE_UPDATED = "project_storage_updated".freeze + PROJECT_STORAGE_DESTROYED = "project_storage_destroyed".freeze + + ROLE_UPDATED = "role_updated".freeze + ROLE_DESTROYED = "role_destroyed".freeze + WATCHER_ADDED = 'watcher_added'.freeze - WATCHER_REMOVED = 'watcher_removed'.freeze + WATCHER_DESTROYED = 'watcher_destroyed'.freeze end end diff --git a/lib/open_project/journal_formatter/attachment.rb b/lib/open_project/journal_formatter/attachment.rb index 9b323b8c4fb..d900aec1590 100644 --- a/lib/open_project/journal_formatter/attachment.rb +++ b/lib/open_project/journal_formatter/attachment.rb @@ -44,11 +44,19 @@ class OpenProject::JournalFormatter::Attachment < JournalFormatter::Base value = format_html_attachment_detail(key.to_s.sub('attachments_', ''), value) end - render_binary_detail_text(label, value, old_value) + render_attachment_detail_text(label, value, old_value) end private + def render_attachment_detail_text(label, value, old_value) + if value.blank? + I18n.t(:text_journal_attachment_deleted, label:, old: old_value) + else + I18n.t(:text_journal_attachment_added, label:, value:) + end + end + def label(_key) Attachment.model_name.human end diff --git a/lib/open_project/journal_formatter/cause.rb b/lib/open_project/journal_formatter/cause.rb index b24a2568ae6..c6f9893915d 100644 --- a/lib/open_project/journal_formatter/cause.rb +++ b/lib/open_project/journal_formatter/cause.rb @@ -44,15 +44,19 @@ class OpenProject::JournalFormatter::Cause < JournalFormatter::Base private - def cause_type_translation(_type) - # currently only date changes have a cause, but in the future when other changes have a cause, - # those can be changed here. - - I18n.t('journals.caused_changes.dates_changed') + def cause_type_translation(type) + case type + when 'system_update' + I18n.t("journals.caused_changes.system_update") + else + I18n.t("journals.caused_changes.dates_changed") + end end def cause_description(cause, html) case cause['type'] + when 'system_update' + system_update_message(cause) when 'working_days_changed' working_days_changed_message(cause['changed_days']) else @@ -60,6 +64,10 @@ class OpenProject::JournalFormatter::Cause < JournalFormatter::Base end end + def system_update_message(cause) + I18n.t("journals.cause_descriptions.system_update.#{cause['feature']}") + end + def related_work_package_changed_message(cause, html) related_work_package = WorkPackage.includes(:project).visible(User.current).find_by(id: cause['work_package_id']) diff --git a/lib/open_project/journal_formatter/custom_field.rb b/lib/open_project/journal_formatter/custom_field.rb index 2f85550dc37..f54c098b52e 100644 --- a/lib/open_project/journal_formatter/custom_field.rb +++ b/lib/open_project/journal_formatter/custom_field.rb @@ -57,6 +57,8 @@ class OpenProject::JournalFormatter::CustomField < JournalFormatter::Base :find_list_value when 'user' :find_user_value + when 'version' + :find_version_value else :format_value end @@ -82,13 +84,7 @@ class OpenProject::JournalFormatter::CustomField < JournalFormatter::Base .where(id: ids) .index_by(&:id) - ids.map do |id| - if user_lookup.key?(id) - user_lookup[id].name - else - I18n.t(:label_missing_or_hidden_custom_option) - end - end.join(', ') + ids_to_names(ids, user_lookup) end def find_list_value(value, custom_field) @@ -104,4 +100,27 @@ class OpenProject::JournalFormatter::CustomField < JournalFormatter::Base id_value[id] || I18n.t(:label_deleted_custom_option) end.join(', ') end + + def find_version_value(value, _custom_field) + ids = value.split(",").map(&:to_i) + + # Lookup visible versions we can find + version_lookup = + Version + .visible(User.current) + .where(id: ids) + .index_by(&:id) + + ids_to_names(ids, version_lookup) + end + + def ids_to_names(ids, id_to_name_lookup) + ids.map do |id| + if id_to_name_lookup.key?(id) + id_to_name_lookup[id].name + else + I18n.t(:label_missing_or_hidden_custom_option) + end + end.join(', ') + end end diff --git a/lib/open_project/journal_formatter/file_link.rb b/lib/open_project/journal_formatter/file_link.rb index 93503b0339b..8fa66e4e1f9 100644 --- a/lib/open_project/journal_formatter/file_link.rb +++ b/lib/open_project/journal_formatter/file_link.rb @@ -32,27 +32,64 @@ class OpenProject::JournalFormatter::FileLink < JournalFormatter::Base include OpenProject::ObjectLinking def render(key, values, options = { html: true }) - id = key.to_s.sub('file_links_', '') - label, old_value, value = format_details(id, values) + id = key.to_s.sub('file_links_', '').to_i + label, old_value, value, storage = format_details(id, values) if options[:html] - label, old_value, value = *format_html_details(label, old_value, value) + label, old_value, value = format_html_details(label, old_value, value) value = format_html_file_link_detail(id, value) end - render_binary_detail_text(label, value, old_value) + render_file_link_detail_text(label, value, old_value, storage) end private + def format_details(id, values) + old_value, current_value = values + [ + label(nil), + old_value&.fetch('link_name'), + current_value&.fetch('link_name'), + storage(id, old_value, current_value) + ] + end + + def storage(id, old, current) + non_nil_value = [old, current].compact.first + file_link = file_link_for(id, non_nil_value) + + non_nil_value['storage_name'] || file_link&.storage&.name || I18n.t('storages.unknown_storage') + end + + def render_file_link_detail_text(label, value, old_value, storage) + if value.blank? + I18n.t(:text_journal_file_link_deleted, label:, old: old_value, storage:) + else + I18n.t(:text_journal_file_link_added, label:, value:, storage:) + end + end + # Based this off the Attachment formatter. Not sure if it is the best approach - def label(_key) = Storages::FileLink.model_name.human + def label(_key) = I18n.t('activerecord.models.file_link') + + def file_link_for(key, value) + value.present? && ::Storages::FileLink.find_by(id: key) + end def format_html_file_link_detail(key, value) - if value.present? && file_link = ::Storages::FileLink.find_by(id: key.to_i) + if file_link = file_link_for(key, value) link_to_file_link(file_link, only_path: false) elsif value.present? value end end + + def link_to_file_link(file_link, options = {}) + text = options.delete(:text) || file_link.origin_name + + link_to text, + url_to_file_link(file_link, only_path: options.delete(:only_path) { true }), + options + end end diff --git a/lib/open_project/object_linking.rb b/lib/open_project/object_linking.rb index 2dc213bceea..e16af57663f 100644 --- a/lib/open_project/object_linking.rb +++ b/lib/open_project/object_linking.rb @@ -79,14 +79,6 @@ module OpenProject options end - def link_to_file_link(file_link, options = {}) - text = options.delete(:text) || file_link.origin_name - - link_to text, - url_to_file_link(file_link, only_path: options.delete(:only_path) { true }), - options - end - # Generates a link to a SCM revision # Options: # * :text - Link text (default to the formatted revision) diff --git a/config/initializers/livingstyleguide_patches.rb b/lib/open_project/patches/lookbook_tree_node_inflector.rb similarity index 77% rename from config/initializers/livingstyleguide_patches.rb rename to lib/open_project/patches/lookbook_tree_node_inflector.rb index 9d27512defd..dbbeda1ffc1 100644 --- a/config/initializers/livingstyleguide_patches.rb +++ b/lib/open_project/patches/lookbook_tree_node_inflector.rb @@ -25,21 +25,23 @@ # # See COPYRIGHT and LICENSE files for more details. #++ -# -if defined?(LivingStyleGuide) - ## - # Override CSS to never be called - module DocumentTemplatePatch + +module OpenProject + module Patches ## - # Define our own template - def template_erb - if @template == :layout - File.read(Rails.root.join('app/views/layouts/styleguide/styleguide.layout.html.erb')) - else + # Allow directory labels in lookbook to be inflected + module LookbookTreeNodeInflector + def label + return name if name == 'OpenProject' + super end end end - - LivingStyleGuide::Document.prepend DocumentTemplatePatch +end + +if Rails.env.development? + OpenProject::Patches.patch_gem_version 'lookbook', '2.0.5' do + Lookbook::TreeNode.prepend OpenProject::Patches::LookbookTreeNodeInflector + end end diff --git a/lib/open_project/scm/manageable_repository.rb b/lib/open_project/scm/manageable_repository.rb index b8557508592..6fc2f76bdab 100644 --- a/lib/open_project/scm/manageable_repository.rb +++ b/lib/open_project/scm/manageable_repository.rb @@ -34,8 +34,8 @@ module OpenProject ## # Take note when projects are renamed and check for associated managed repositories - OpenProject::Notifications.subscribe('project_renamed') do |payload| - repository = payload[:project].repository + OpenProject::Notifications.subscribe(OpenProject::Events::PROJECT_RENAMED) do |payload| + repository = payload[:project]&.repository if repository&.managed? ::SCM::RelocateRepositoryJob.perform_later(repository) diff --git a/lib/open_project/static/links.rb b/lib/open_project/static/links.rb index 25ef806c985..3b24469b797 100644 --- a/lib/open_project/static/links.rb +++ b/lib/open_project/static/links.rb @@ -243,6 +243,9 @@ module OpenProject attribute_highlighting: { href: 'https://www.openproject.org/docs/user-guide/work-packages/work-package-table-configuration/#attribute-highlighting-enterprise-add-on' }, + boards: { + href: 'https://www.openproject.org/docs/user-guide/agile-boards/#action-boards-enterprise-add-on' + }, custom_field_projects: { href: 'https://www.openproject.org/docs/system-admin-guide/custom-fields/custom-fields-projects/' }, diff --git a/lib/open_project/text_formatting/formats/markdown/pandoc_downloader.rb b/lib/open_project/text_formatting/formats/markdown/pandoc_downloader.rb deleted file mode 100644 index bdfbc365094..00000000000 --- a/lib/open_project/text_formatting/formats/markdown/pandoc_downloader.rb +++ /dev/null @@ -1,129 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2023 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require 'open3' -require 'fileutils' -require 'rest-client' - -module OpenProject::TextFormatting::Formats - module Markdown - module PandocDownloader - class << self - def check_or_download! - # Return if we have another version defined in ENV - if forced_pandoc_path - return compatible? forced_pandoc_path, - "Environment variable OPENPROJECT_PANDOC_PATH set, but version is not executable by OpenProject or incompatible." - end - - # Return if we have a compatible version - return if compatible? - - warn <<~INFO - You have no compatible (>= 2.0) of pandoc in path. We're now trying to download a recent version for your amd64 linux - from '#{pandoc_amd64_tar}' to '#{vendored_pandoc_dir}'. - - For more information, please visit this page: https://www.openproject.org/textile-to-markdown-migration - INFO - - download! - - raise "Failed to download pandoc version" unless compatible? - rescue StandardError => e - warn <<~WARNING - Error occurred while trying to find / download compatible pandoc version for your system: - - #{e.message} - WARNING - end - - ## - # Check if the given pandoc version is compatible - # Returns true/false - # Raises raise_msg if set and incompatible - def compatible?(path = pandoc_path, raise_msg = nil) - stdout, _, status = Open3.capture3(path, '--version') - - if !status.success? && raise_msg.present? - raise raise_msg - end - - status.success? && stdout.match(/^pandoc [23]\./i) - rescue StandardError => e - if raise_msg.present? - raise raise_msg - end - - false - end - - def forced_pandoc_path - ENV.fetch('OPENPROJECT_PANDOC_PATH', nil) - end - - def pandoc_path - vendored = Rails.root.join('vendor/pandoc/bin/pandoc').to_s - - # Always return the vendored path if we have installed one - return vendored if File.executable?(vendored) - - ENV.fetch('OPENPROJECT_PANDOC_PATH', 'pandoc') - end - - def vendored_pandoc_dir - Rails.root.join('vendor/pandoc').to_s - end - - def pandoc_amd64_tar - ENV.fetch('OPENPROJECT_PANDOC_TAR_DOWNLOAD', 'https://github.com/jgm/pandoc/releases/download/2.2.3.2/pandoc-2.2.3.2-linux.tar.gz') - end - - private - - def download! - response = RestClient::Request.execute method: :get, - url: pandoc_amd64_tar, - raw_response: true - tempfile = response.file - - # Create vendor dir, this will however usually exist already - FileUtils.mkdir_p vendored_pandoc_dir - - begin - # Extract downloaded tar into vendor - _, stderr_str, status = Open3.capture3('tar', 'xvzf', tempfile.path.to_s, - '--strip-components', '1', '-C', vendored_pandoc_dir) - raise stderr_str unless status.success? - ensure - tempfile.unlink - end - end - end - end - end -end diff --git a/lib/open_project/text_formatting/formats/markdown/pandoc_wrapper.rb b/lib/open_project/text_formatting/formats/markdown/pandoc_wrapper.rb deleted file mode 100644 index 24b8ff91067..00000000000 --- a/lib/open_project/text_formatting/formats/markdown/pandoc_wrapper.rb +++ /dev/null @@ -1,139 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2023 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require 'posix-spawn' - -module OpenProject::TextFormatting::Formats - module Markdown - class PandocWrapper - attr_reader :logger - - def initialize(logger = ::ActiveSupport::Logger.new(STDOUT)) - @logger = logger - end - - def execute!(stdin) - PandocDownloader.check_or_download! - run_pandoc! pandoc_arguments, stdin_data: stdin - end - - def check_arguments! - PandocDownloader.check_or_download! - wrap_mode - read_output_formats - end - - def pandoc_arguments - [ - wrap_mode, - '--atx-headers', - '-f', - 'textile', - '-t', - 'commonmark' - ] - end - - ## - # Detect available wrap mode - # --wrap=preserve will keep the wrapping the same, however is only available in versions 1.16+ - # In older versions we try to use the deprecated --no-wrap instead - # --atx-headers will lead to headers like `### Some header` and '## Another header' - def wrap_mode - @wrap_mode ||= begin - usage = read_usage_string - - # Detect wrap usage - if usage.include? '--wrap=' - '--wrap=preserve' - elsif usage.include? '--no-wrap' - '--no-wrap' - else - err = 'Your pandoc version has neither --no-wrap nor --wrap=preserve. Please install a recent version of pandoc.' - logger.error err - raise err - end - end - end - - def pandoc_timeout - ENV.fetch('OPENPROJECT_PANDOC_TIMEOUT_SECONDS', 30).to_i - end - - private - - ## - # Run pandoc through posix-spawn and raise if an exception occurred - def run_pandoc!(command, stdin_data: nil, timeout: pandoc_timeout) - child = POSIX::Spawn::Child.new(PandocDownloader.pandoc_path, *command, input: stdin_data, timeout:) - status = child.status - - unless status.success? - code = status.exitstatus || 'unknown status (killed?)' - termsig = status.termsig || 'none' - stopsig = status.stopsig || 'none' - signal_msg = - if status.signaled? - "Process received signal (term #{termsig}, stop #{stopsig})" - else - "Process did not receive signal" - end - - out = (child.out || '').force_encoding('UTF-8').truncate(100) - err = (child.err || '').force_encoding('UTF-8') - raise <<~ERRORSTR - Pandoc failed with code [#{code}] [Stopped=#{status.stopped?}] - #{signal_msg} - #{' '} - #{out} - #{err} - ERRORSTR - end - - # posix-spawn forces binary output, however pandoc - # only works with UTF-8 - child.out.force_encoding('UTF-8') - rescue POSIX::Spawn::TimeoutExceeded => e - raise Timeout::Error, "Timeout occurred while running pandoc: #{e.message}" - end - - def read_usage_string - run_pandoc! %w[--help] - end - - def read_output_formats - @output_formats ||= begin - run_pandoc! %w[--list-output-formats] - rescue StandardError => e - logger.warn "Failed to detect output format (Error was: #{e}). Falling back to github_markdown" - '' - end - end - end - end -end diff --git a/lib/open_project/text_formatting/formats/markdown/textile_converter.rb b/lib/open_project/text_formatting/formats/markdown/textile_converter.rb deleted file mode 100644 index 7c5ff4e84b7..00000000000 --- a/lib/open_project/text_formatting/formats/markdown/textile_converter.rb +++ /dev/null @@ -1,518 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2023 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -# This file is derived from the 'Redmine converter from Textile to Markown' -# https://github.com/Ecodev/redmine_convert_textile_to_markown -# -# Original license: -# Copyright (c) 2016 -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -require 'ruby-progressbar' - -module OpenProject::TextFormatting::Formats - module Markdown - class TextileConverter - include ActionView::Helpers::TagHelper - - TAG_CODE = 'pandoc-unescaped-single-backtick'.freeze - TAG_FENCED_CODE_BLOCK = 'force-pandoc-to-ouput-fenced-code-block'.freeze - DOCUMENT_BOUNDARY = "TextileConverterDocumentBoundary09339cab-f4f4-4739-85b0-d02ba1f342e6".freeze - BLOCKQUOTE_START = "TextileConverterBlockquoteStart09339cab-f4f4-4739-85b0-d02ba1f342e6".freeze - BLOCKQUOTE_END = "TextileConverterBlockquoteEnd09339cab-f4f4-4739-85b0-d02ba1f342e6".freeze - - attr_reader :pandoc, :logger - - def initialize - @logger = ::OpenProject::Logging::TeeLogger.new 'markdown-migration' - @pandoc = PandocWrapper.new logger - end - - def run! - # We're going to decrease the AR logger to avoid dumping all SQL inserts - # in the output logs - current_ar_log_level = ActiveRecord::Base.logger.level - - begin - logger.info 'Starting conversion of Textile fields to CommonMark.' - - logger.info 'Decreasing ActiveRecord logger to avoid flooding your logs.' - ActiveRecord::Base.logger.level = :error - - logger.info 'Checking compatibility of your installed pandoc version.' - pandoc.check_arguments! - - ActiveRecord::Base.transaction do - converters.each(&:call) - end - - logger.info "\n-- Completed --" - ensure - ActiveRecord::Base.logger.level = current_ar_log_level - end - end - - private - - def converters - [ - method(:convert_settings), - method(:convert_models), - method(:convert_custom_field_longtext) - ] - end - - def convert_settings - logger.info 'Converting settings ' - Setting.welcome_text = convert_textile_to_markdown(Setting.welcome_text) - - Setting.registration_footer = Setting.registration_footer.dup.tap do |footer| - footer.transform_values { |val| convert_textile_to_markdown(val) } - end - end - - ## - # Converting model attributes as defined in +models_to_convert+. - def convert_models - models_to_convert.each do |klass, attributes| - logger.info "Converting #{klass.name.pluralize} " - - # Iterate in batches to avoid plucking too much - with_original_values_in_batches(klass, attributes) do |orig_values| - markdowns_in_groups = bulk_convert_textile_with_fallback(klass, orig_values, attributes) - new_values = new_values_for(attributes, orig_values, markdowns_in_groups) - - next if new_values.empty? - - ActiveRecord::Base.connection.execute(batch_update_statement(klass, attributes, new_values)) - end - end - end - - def convert_custom_field_longtext - formattable_cfs = CustomField.where(field_format: 'text').pluck(:id) - logger.info "CustomField type text " - scope = CustomValue.where(custom_field_id: formattable_cfs) - progress = ProgressBar.create(title: "Formattable CustomValues", total: scope.count) - scope.in_batches(of: 200) do |relation| - relation.pluck(:id, :value).each do |cv_id, value| - CustomValue.where(id: cv_id).update_all(value: convert_textile_to_markdown(value)) - progress.increment - end - end - progress.finish - end - - # Iterate in batches to avoid plucking too much - def with_original_values_in_batches(klass, attributes) - batches_of_objects_to_convert(klass, attributes) do |relation, progress| - orig_values = relation.pluck(:id, *attributes) - - result = yield orig_values - progress.progress += orig_values.count - - result - end - end - - def bulk_convert_textile_with_fallback(klass, orig_values, attributes) - old_values = orig_values.inject([]) do |former_values, values| - former_values + values.drop(1) - end - - joined_textile = concatenate_textile(old_values) - - markdowns_in_groups = [] - - begin - markdown = convert_textile_to_markdown(joined_textile, raise_on_timeout: true) - markdowns_in_groups = split_markdown(markdown).each_slice(attributes.length).to_a - rescue StandardError => e - # Don't do anything. Let the subsequent code try to handle it again - end - - if markdowns_in_groups.length != orig_values.length - # Error handling: Some textile seems to be malformed e.g.
    something
    ). - # In such cases, handle texts individually to avoid the error affecting other texts - progress = ProgressBar.create(title: "Converting items individually due to pandoc mismatch", total: orig_values.length) - markdowns = old_values.each_with_index.map do |old_value, index| - convert_textile_to_markdown(old_value, raise_on_timeout: false) - rescue StandardError - logger.error "Failing to convert single document #{klass.name} ##{orig_values[index].first}. " - non_convertible_textile_doc(old_value) - ensure - progress.increment - end - - markdowns_in_groups = markdowns.each_slice(attributes.length).to_a - progress.finish - end - - markdowns_in_groups - end - - def new_values_for(attributes, orig_values, markdowns_in_groups) - new_values = [] - orig_values.each_with_index do |values, index| - new_values << { id: values[0] }.merge(attributes.zip(markdowns_in_groups[index]).to_h) - end - - new_values - end - - def convert_textile_to_markdown(textile, raise_on_timeout: false) - return '' unless textile.present? - - cleanup_before_pandoc(textile) - - markdown = execute_pandoc_with_stdin! textile, raise_on_timeout - - if markdown.empty? - markdown - else - cleanup_after_pandoc(markdown) - end - end - - def execute_pandoc_with_stdin!(textile, raise_on_timeout) - pandoc.execute! textile - rescue Timeout::Error => e - if raise_on_timeout - logger.error <<~TIMEOUT_WARN - Execution of pandoc timed out: #{e}. - - You may want to increase the timeout - (OPENPROJECT_PANDOC_TIMEOUT_SECONDS, currently at #{pandoc.pandoc_timeout} seconds) - TIMEOUT_WARN - - raise e - else - logger.error <<~TIMEOUT_WARN - Execution of pandoc timed out: #{e}. - - The document will not be replaced (probably due to syntax errors) because pandoc did not finish. - If you're running PostgreSQL: You can try to cancel this run and retry the migration and with an increased timeout - (OPENPROJECT_PANDOC_TIMEOUT_SECONDS, currently at #{pandoc.pandoc_timeout} seconds). - - If you're running MySQL: You need to restore your pre-upgrade backup first since it does not have transactional DDL. - - However, please note that pandoc sometimes trip up over specific textile parts that cause it to run indefinitely. - In this case, you will have to manually fix the textile and increasing the timeout will achieve nothing. - TIMEOUT_WARN - - non_convertible_textile_doc(textile) - end - rescue StandardError => e - logger.error "Execution of pandoc failed: #{e}" - raise e - end - - def non_convertible_textile_doc(textile) - <<~DOC - # Warning: This document could not be converted, probably due to syntax errors. - The below content is textile. - - -
    -
    -          #{textile}
    -
    -          
    - DOC - end - - def models_to_convert - { - ::Announcement => [:text], - ::AttributeHelpText => [:help_text], - ::Comment => [:comments], - ::WikiPage => [:text], - ::WorkPackage => [:description], - ::Message => [:content], - ::News => [:description], - OldForum => [:description], - ::Project => [:description], - ::Journal => [:notes], - ::Journal::MessageJournal => [:content], - ::Journal::WikiPageJournal => [:text], - ::Journal::WorkPackageJournal => [:description], - ::AttributeHelpText => [:help_text] - } - end - - def batches_of_objects_to_convert(klass, attributes) - scopes = attributes.map { |attribute| klass.where.not(attribute => nil).where.not(attribute => '') } - - scope = scopes.shift - scopes.each do |attribute_scope| - scope = scope.or(attribute_scope) - end - - # Iterate in batches to avoid plucking too much - progress = ProgressBar.create(title: "Conversion", starting_at: 0, total: scope.count) - scope.in_batches(of: 50) do |relation| - yield relation, progress - end - progress.finish - end - - def batch_update_statement(klass, attributes, values) - table_name = klass.table_name - sets = attributes.map { |a| "#{a} = new_values.#{a}" }.join(', ') - new_values = values.map do |value_hash| - text_values = value_hash.except(:id).map { |_, v| ActiveRecord::Base.connection.quote(v) }.join(', ') - "(#{value_hash[:id]}, #{text_values})" - end - - <<-SQL - UPDATE #{table_name} - SET - #{sets} - FROM ( - VALUES - #{new_values.join(', ')} - ) AS new_values (id, #{attributes.join(', ')}) - WHERE #{table_name}.id = new_values.id - SQL - end - - def concatenate_textile(textiles) - textiles.join("\n\n#{DOCUMENT_BOUNDARY}\n\n") - end - - def split_markdown(markdown) - markdown.split("\n\n#{DOCUMENT_BOUNDARY}\n\n") - end - - def cleanup_before_pandoc(textile) - placeholder_for_inline_code_at(textile) - drop_table_colspan_notation(textile) - drop_table_alignment_notation(textile) - move_class_from_code_to_pre(textile) - remove_code_inside_pre(textile) - convert_malformed_textile(textile) - remove_empty_paragraphs(textile) - replace_numbered_headings(textile) - add_newline_to_avoid_lazy_blocks(textile) - remove_spaces_before_table(textile) - hard_breaks_within_multiline_tables(textile) - wrap_blockquotes(textile) - end - - def cleanup_after_pandoc(markdown) - # Remove the \ pandoc puts before * and > at beginning of lines - markdown.gsub!(/^((\\[*>])+)/) { $1.gsub("\\", "") } - - # Add a blank line before lists - # But do not apply it to *emphasis* or **strong** at the start of a line (whitespace is important) - markdown.gsub!(/^([^*].*)\n\* /, "\\1\n\n* ") - - # Remove the injected tag - markdown.gsub!(' ' + TAG_FENCED_CODE_BLOCK, '') - - # Replace placeholder with real backtick - markdown.gsub!(TAG_CODE, '`') - - # Un-escape Redmine link syntax to wiki pages - markdown.gsub!('\[\[', '[[') - markdown.gsub!('\]\]', ']]') - - # replace filtered out span with ins - markdown.gsub!(/(.+)<\/span>/, '\1') - - markdown.gsub!(/#{BLOCKQUOTE_START}\n(.+?)\n\n#{BLOCKQUOTE_END}/m) do - $1.gsub(/(\n)([^\n]*)/, '\1> \2') - end - - # Create markdown links from !image!:link syntax - # ![alt](image]) - markdown.gsub! /(?!\[[^\]]*\]\([^)]+\)):(?https?:\S+)/, - '[\k](\k)' - - # remove the escaping from links within parenthesis having a trailing slash - # ([description](https://some/url/\)) - markdown.gsub! /\((?.*?)\[(?.*?)\]\((?.*?)\\\)\)/, - '(\k[\k](\k))' - - convert_macro_syntax(markdown) - - markdown - end - - # Convert old {{macroname(args)}} syntax to - def convert_macro_syntax(markdown) - old_macro_regex = / - (!)? # escaping - ( - \{\{ # opening tag - ([\w\\_]+) # macro name - (\(([^}]*)\))? # optional arguments - \}\} # closing tag - ) - /x - - markdown.gsub!(old_macro_regex) do - esc = $1 - all = $2 - macro = $3.gsub('\_', '_') - args = $5 || '' - args_array = args.split(',').each(&:strip!) - data = {} - - # Escaped macros should probably render as before? - next all if esc.present? - - case macro - when 'timeline' - next content_tag :macro, I18n.t('macros.legacy_warning.timeline'), class: 'legacy-macro -macro-unavailable' - when 'hello_world' - next '' - when 'include' - macro = 'include_wiki_page' - data[:page] = args - when 'create_work_package_link' - data[:type] = args_array[0] if args_array.length >= 1 - data[:classes] = args_array[1] if args_array.length >= 2 - else - data[:arguments] = args if args.present? - end - - content_tag :macro, '', class: macro, data: - end - end - - # OpenProject support @ inside inline code marked with @ (such as "@git@github.com@"), but not pandoc. - # So we inject a placeholder that will be replaced later on with a real backtick. - def placeholder_for_inline_code_at(textile) - textile.gsub!(/@(\S+@\S+)@/, TAG_CODE + '\\1' + TAG_CODE) - end - - # Drop table colspan/rowspan notation ("|\2." or "|/2.") because pandoc does not support it - # See https://github.com/jgm/pandoc/issues/22 - def drop_table_colspan_notation(textile) - textile.gsub!(/\|[\/\\]\d\. /, '| ') - end - - # Drop table alignment notation ("|>." or "|<." or "|=.") because pandoc does not support it - # See https://github.com/jgm/pandoc/issues/22 - def drop_table_alignment_notation(textile) - textile.gsub!(/\|[<>=]\. /, '| ') - end - - # Move the class from to
     so pandoc can generate a code block with correct language
    -      def move_class_from_code_to_pre(textile)
    -        textile.gsub!(/()/, '\\1\\3\\2\\4')
    -      end
    -
    -      # Remove the  directly inside 
    , because pandoc would incorrectly preserve it
    -      def remove_code_inside_pre(textile)
    -        textile.gsub!(/(]*>)/, '\\1')
    -        textile.gsub!(/<\/code>(<\/pre>)/, '\\1')
    -      end
    -
    -      # Some malformed textile content make pandoc run extremely slow,
    -      # so we convert it to proper textile before hitting pandoc
    -      # see https://github.com/jgm/pandoc/issues/3020
    -      def convert_malformed_textile(textile)
    -        textile.gsub!(/-          # (\d+)/, "* \\1")
    -      end
    -
    -      # Remove empty paragraph blocks which trip up pandoc
    -      def remove_empty_paragraphs(textile)
    -        textile.gsub!(/\np(=|>)?\.[\s.]*\n/, '')
    -      end
    -
    -      # Replace numbered headings as they are not supported in commonmark/gfm
    -      def replace_numbered_headings(textile)
    -        textile.gsub!(/h(\d+)#./, 'h\\1.')
    -      end
    -
    -      # Add an additional newline before:
    -      # * every pre block prefixed by only one newline
    -      #   as indented code blocks do not interrupt a paragraph and also do not have precedence
    -      #   compared to a list which might also be indented.
    -      # * every table prefixed by only one newline (if the line before isn't a table already)
    -      # * every blockquote prefixed by one newline (if the line before isn't a blockquote)
    -      def add_newline_to_avoid_lazy_blocks(textile)
    -        textile.gsub!(/(\n[^>|]+)\r?\n(\s*)(
    |\||>)/, "\\1\n\n\\2\\3")
    -      end
    -
    -      # Remove spaces before a table as that would lead to the table
    -      # not being identified
    -      def remove_spaces_before_table(textile)
    -        textile.gsub!(/^\s+(\|.+?\|)/, "\n\n\\1")
    -      end
    -
    -      ##
    -      # Redmine introduced hard breaks to support multiline tables that are not official
    -      # textile. We detect tables with line breaks and replace them with 
    - # https://www.redmine.org/projects/redmine/repository/revisions/2824/diff/ - def hard_breaks_within_multiline_tables(textile) - content_regexp = %r{ - (?<=\|) # Assert beginning table pipe lookbehind - ([^|]{5,}) # Non-empty content - (?=\|) # Assert ending table pipe lookahead - }mx - - # Match all textile tables - textile.gsub!(/^(\|.+?\|)$/m) do |table| - table.gsub(content_regexp) { |table_content| table_content.gsub("\n", "
    ") } - end - end - - # Wrap all blockquote blocks into boundaries as `>` is not valid blockquote syntax and would thus be - # escaped - def wrap_blockquotes(textile) - textile.gsub!(/((\n>[^\n]*)+)/m) do - "\n#{BLOCKQUOTE_START}\n" + $1.gsub(/(\n)> *([^\n]*)/, '\1\2') + "\n\n#{BLOCKQUOTE_END}\n" - end - end - - class OldForum < ApplicationRecord - self.table_name = 'boards' - end - end - end -end diff --git a/lib/redmine/menu_manager/menu_helper.rb b/lib/redmine/menu_manager/menu_helper.rb index ae26aefc2a2..9adb563613a 100644 --- a/lib/redmine/menu_manager/menu_helper.rb +++ b/lib/redmine/menu_manager/menu_helper.rb @@ -31,6 +31,7 @@ module Redmine::MenuManager::MenuHelper include ::Redmine::MenuManager::WikiMenuHelper include AccessibilityHelper include IconsHelper + include IconsHelper delegate :current_menu_item, to: :controller @@ -170,7 +171,7 @@ module Redmine::MenuManager::MenuHelper class: 'toggler main-menu-toggler', type: :button, data: { action: 'menus--main#descend' }) do - spot_icon('arrow-right3', size: '1') + render(Primer::Beta::Octicon.new("arrow-right", size: :small)) end end @@ -210,7 +211,7 @@ module Redmine::MenuManager::MenuHelper def render_children_back_up_link content_tag( :a, - spot_icon('arrow-left1', size: '1_25'), + render(Primer::Beta::Octicon.new("arrow-left", size: :small)), title: I18n.t('js.label_up'), class: 'main-menu--arrow-left-to-project', data: { diff --git a/lib/services/remove_watcher.rb b/lib/services/remove_watcher.rb index 92bb78113d1..f0dad9d43c5 100644 --- a/lib/services/remove_watcher.rb +++ b/lib/services/remove_watcher.rb @@ -33,12 +33,12 @@ class Services::RemoveWatcher end def run(success: -> {}, failure: -> {}) - watcher = @work_package.watchers.find_by_user_id(@user.id) + watcher = @work_package.watchers.find_by(user_id: @user.id) if watcher.present? @work_package.watcher_users.delete(@user) success.call - OpenProject::Notifications.send(OpenProject::Events::WATCHER_REMOVED, + OpenProject::Notifications.send(OpenProject::Events::WATCHER_DESTROYED, watcher:, watcher_remover: User.current) else diff --git a/lib/tabular_form_builder.rb b/lib/tabular_form_builder.rb index 09b30c678cc..0a2106ca596 100644 --- a/lib/tabular_form_builder.rb +++ b/lib/tabular_form_builder.rb @@ -112,6 +112,10 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder inputs['show-ignore-non-working-days'] = options[:show_ignore_non_working_days] end + if options[:required] + inputs[:required] = options[:required] + end + label = label_for_field(field, label_options) input = angular_component_tag('op-basic-single-date-picker', class: options[:class], diff --git a/lib_static/open_project/configuration/helpers.rb b/lib_static/open_project/configuration/helpers.rb index 018406951fb..98e44561058 100644 --- a/lib_static/open_project/configuration/helpers.rb +++ b/lib_static/open_project/configuration/helpers.rb @@ -190,6 +190,10 @@ module OpenProject Integer(ENV['STATSD_PORT'].presence || statsd['port'].presence) end + def lookbook_enabled? + self['lookbook_enabled'] + end + private ## diff --git a/lib_static/plugins/acts_as_journalized/lib/acts/journalized/file_link_journal_differ.rb b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/file_link_journal_differ.rb new file mode 100644 index 00000000000..9b0bbefd20c --- /dev/null +++ b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/file_link_journal_differ.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Acts::Journalized + module FileLinkJournalDiffer + class << self + def get_changes_to_file_links(predecessor, storable_journals) + if predecessor.nil? + storable_journals.each_with_object({}) do |journal, hash| + change_key = "file_links_#{journal.file_link_id}" + new_values = { link_name: journal.link_name, storage_name: journal.storage_name } + hash[change_key] = [nil, new_values] + end + else + current_storables = storable_journals.map(&:attributes) + previous_storables = predecessor.storable_journals.map(&:attributes) + + changes_on_file_links(previous_storables, current_storables) + end + end + + def changes_on_file_links(previous, current) + ids = all_file_link_ids(previous, current) + + cleanup_changes( + pair_changes(ids, previous, current) + ).transform_keys! { |key| "file_links_#{key}" } + end + + def all_file_link_ids(previous, current) + current.pluck('file_link_id') | previous.pluck('file_link_id') + end + + def cleanup_changes(changes) = changes.reject { |_, (first, last)| first == last } + + def pair_changes(ids, previous, current) + ids.index_with do |id| + [select_journals(previous.select { |attributes| attributes['file_link_id'] == id }), + select_journals(current.select { |attributes| attributes['file_link_id'] == id })] + end + end + + def select_journals(journals) + return if journals.empty? + + journals.sort.map { |hash| hash.slice('link_name', 'storage_name') }.last + end + end + end +end diff --git a/lib_static/plugins/acts_as_journalized/lib/acts/journalized/journable_differ.rb b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/journable_differ.rb index fbfe65915b8..86f7fad034a 100644 --- a/lib_static/plugins/acts_as_journalized/lib/acts/journalized/journable_differ.rb +++ b/lib_static/plugins/acts_as_journalized/lib/acts/journalized/journable_differ.rb @@ -38,8 +38,8 @@ module Acts::Journalized .with_indifferent_access end - def association_changes(original, changed, *association_params) - get_association_changes(original, changed, *association_params) + def association_changes(original, changed, *) + get_association_changes(original, changed, *) end private @@ -90,12 +90,12 @@ module Acts::Journalized def added_references(merged_references) merged_references - .select { |_, (old_value, new_value)| old_value.nil? && new_value.present? } + .select { |_, (old_value, new_value)| old_value.to_s.empty? && new_value.present? } end def removed_references(merged_references) merged_references - .select { |_, (old_value, new_value)| old_value.present? && new_value.nil? } + .select { |_, (old_value, new_value)| old_value.present? && new_value.to_s.empty? } end def changed_references(merged_references) diff --git a/lib_static/plugins/acts_as_journalized/lib/journal_changes.rb b/lib_static/plugins/acts_as_journalized/lib/journal_changes.rb index f157f3814f1..10d6e9e7820 100644 --- a/lib_static/plugins/acts_as_journalized/lib/journal_changes.rb +++ b/lib_static/plugins/acts_as_journalized/lib/journal_changes.rb @@ -63,13 +63,9 @@ module JournalChanges if has_file_links? @changes.merge!( - ::Acts::Journalized::JournableDiffer.association_changes( + ::Acts::Journalized::FileLinkJournalDiffer.get_changes_to_file_links( predecessor, - self, - 'storable_journals', - 'file_links', - :file_link_id, - :link_name + storable_journals ) ) end diff --git a/modules/bim/app/views/bim/bcf/issues/configure_unknown_mails.html.erb b/modules/bim/app/views/bim/bcf/issues/configure_unknown_mails.html.erb index c85b0d27456..81bb290e0df 100644 --- a/modules/bim/app/views/bim/bcf/issues/configure_unknown_mails.html.erb +++ b/modules/bim/app/views/bim/bcf/issues/configure_unknown_mails.html.erb @@ -20,6 +20,7 @@ title: t(:label_role_search), tabIndex: 0, container_class: '-slim', + class: 'option-label--select', id: 'unknown_mails_invite_role_ids' %> (<%= t('bcf.recommended') %>) @@ -59,4 +60,3 @@ defaults_bcf_project_ifc_models_path(@project), class: 'button' %> <% end %> - diff --git a/modules/bim/config/locales/crowdin/cs.seeders.yml b/modules/bim/config/locales/crowdin/cs.seeders.yml index cbb0a97d082..8824b96c2e6 100644 --- a/modules/bim/config/locales/crowdin/cs.seeders.yml +++ b/modules/bim/config/locales/crowdin/cs.seeders.yml @@ -215,7 +215,7 @@ cs: description: This type is hierarchically a parent of the types "Clash" and "Request", thus represents a general note. children: item_0: - subject: Gathering first project information + subject: Shromažďování informací o prvním projektu description: |- ## Goal @@ -228,7 +228,7 @@ cs: * Each need shall represent a task with its corresponding work packages * Derive the cost estimation and time frame item_1: - subject: Summarize the results + subject: Shrňte výsledky description: |- ## Goal @@ -243,7 +243,7 @@ cs: * This overview informs all participants about the decisions made * ... item_2: - subject: End of basic evaluation + subject: Konec základního hodnocení description: This type is hierarchically a parent of the types "Clash" and "Request", thus represents a general note. item_2: subject: Preliminary planning @@ -504,7 +504,7 @@ cs: subject: Completion of the BIM execution plan description: This type is hierarchically a parent of the types "Clash" and "Request", thus represents a general note. item_2: - subject: End of preparation phase + subject: Konec přípravné fáze description: This type is hierarchically a parent of the types "Clash" and "Request", thus represents a general note. item_3: subject: Creating initial BIM model diff --git a/modules/bim/config/locales/crowdin/ne.seeders.yml b/modules/bim/config/locales/crowdin/ne.seeders.yml index b05682d1a0b..b6ee74299dd 100644 --- a/modules/bim/config/locales/crowdin/ne.seeders.yml +++ b/modules/bim/config/locales/crowdin/ne.seeders.yml @@ -603,13 +603,13 @@ ne: description: This type is hierarchically a parent of the types "Clash" and "Request", thus represents a general note. item_6: subject: Modelling & coordinating, second cycle - description: "## Goal\\r\n\\r\n* ...\\r\n\\r\n## Description\\r\n\\r\n* \\ ..." + description: "## Goal\r\n\r\n* ...\r\n\r\n## Description\r\n\r\n* \\ ..." item_7: subject: Modelling & coordinating, ... cycle description: This type is hierarchically a parent of the types "Clash" and "Request", thus represents a general note. item_8: subject: Modelling & coordinating, (n-th minus 1) cycle - description: "## Goal\\r\n\\r\n* ...\\r\n\\r\n## Description\\r\n\\r\n* \\ ..." + description: "## Goal\r\n\r\n* ...\r\n\r\n## Description\r\n\r\n* \\ ..." item_9: subject: Modelling & coordinating n-th cycle description: This type is hierarchically a parent of the types "Clash" and "Request", thus represents a general note. diff --git a/modules/bim/config/locales/crowdin/nl.seeders.yml b/modules/bim/config/locales/crowdin/nl.seeders.yml index 2e69d3d1ff8..95ad8d5e521 100644 --- a/modules/bim/config/locales/crowdin/nl.seeders.yml +++ b/modules/bim/config/locales/crowdin/nl.seeders.yml @@ -13,7 +13,7 @@ nl: item_2: name: Hoog item_3: - name: Critical + name: Kritiek statuses: item_0: name: Nieuw @@ -42,7 +42,7 @@ nl: item_4: name: Remark item_5: - name: Request + name: Verzoeken item_6: name: Clash global_queries: @@ -55,7 +55,7 @@ nl: group_name: Subdocumenten groups: item_0: - name: Architects + name: Architecten item_1: name: BIM Coordinators item_2: @@ -228,7 +228,7 @@ nl: * Each need shall represent a task with its corresponding work packages * Derive the cost estimation and time frame item_1: - subject: Summarize the results + subject: Vat de resultaten samen description: |- ## Goal @@ -265,7 +265,7 @@ nl: * This overview informs all participants about the decisions made * ... item_1: - subject: Summarize results + subject: Resultaten samenvatten description: |- ## Goal @@ -376,7 +376,7 @@ nl: * Electrical installation * ... item_5: - subject: Final touches + subject: Laatste details description: |- ## Goal @@ -417,7 +417,7 @@ nl: item_0: Category 1 (to be changed in Project settings) queries: item_0: - name: Project plan + name: Project abonnement item_1: name: Mijlpalen item_2: @@ -468,7 +468,7 @@ nl: The next step could be to check out the timetable and adjusting the appointments, by looking at the [Gantt chart]({{opSetting:base_url}}/projects/demo-bim-project/work_packages?query_props=%7B%22c%22%3A%5B%22id%22%2C%22subject%22%2C%22startDate%22%2C%22dueDate%22%5D%2C%22tv%22%3Atrue%2C%22tzl%22%3A%22weeks%22%2C%22hi%22%3Atrue%2C%22g%22%3A%22%22%2C%22t%22%3A%22startDate%3Aasc%22%2C%22f%22%3A%5B%7B%22n%22%3A%22status%22%2C%22o%22%3A%22o%22%2C%22v%22%3A%5B%5D%7D%5D%2C%22pa%22%3A1%2C%22pp%22%3A100%7D). item_1: - subject: Project preparation + subject: Voorbereiding project description: This type is hierarchically a parent of the types "Clash" and "Request", thus represents a general note. children: item_0: @@ -686,11 +686,11 @@ nl: item_1: name: Clashes item_2: - name: Requests + name: Verzoeken item_3: name: Remarks item_4: - name: Project plan + name: Project abonnement item_5: name: Mijlpalen item_6: diff --git a/modules/bim/config/locales/crowdin/ru.seeders.yml b/modules/bim/config/locales/crowdin/ru.seeders.yml index 5243ae25bb4..902ce0d24f5 100644 --- a/modules/bim/config/locales/crowdin/ru.seeders.yml +++ b/modules/bim/config/locales/crowdin/ru.seeders.yml @@ -89,7 +89,7 @@ ru: demo-construction-project: name: "(Демо) Конструкторский проект" status_explanation: Все задачи и подпроекты выполняются по графику. Вовлеченные люди знают свои задачи. Система полностью настроена. - description: This is a short summary of the goals of this demo construction project. + description: Это краткое изложение целей этого демонстрационного строительного проекта. news: item_0: title: Добро пожаловать в демо-проект @@ -217,31 +217,31 @@ ru: item_0: subject: Сбор первичной информации о проекте description: |- - ## Goal + ## Цель - * Define tasks based on the customer needs - * Time frame and cost estimation shall be defined + * Определите задачи на основе потребностей клиента + * Должны быть определены временные рамки и оценка затрат - ## Description + ## Описание - * Identify the customer needs by having a workshop with him/ her - * Each need shall represent a task with its corresponding work packages - * Derive the cost estimation and time frame + * Определите потребности клиента, проведя с ним семинар + * Каждая потребность должна представлять собой задачу с соответствующими пакетами работ + * Определите оценку затрат и временные рамки item_1: subject: Подвести итоги description: |- - ## Goal + ## Цель - * Create a useful overview of the results - * Check what has been done and summarize the results - * Communicate all the relevant results with the customer - * Identify the fundamental boundary conditions of the project + * Создать полезный обзор результатов + * Проверить, что было сделано, и обобщить результаты + * Сообщить все соответствующие результаты заказчику + * Определить основные граничные условия проекта - ## Description + ## Описание - * Each topic gets its own overview which will be used as a catalogue of results - * This overview informs all participants about the decisions made - * ... + * Каждая тема получает свой собственный обзор, который будет использоваться в качестве каталога результатов + * Этот обзор информирует всех участников о принятых решениях + * ... item_2: subject: Окончание базовой оценки description: Этот тип является иерархически родителем типов "Clash" и "Request", таким образом представляет общую заметку. @@ -267,18 +267,18 @@ ru: item_1: subject: Суммировать результаты description: |- - ## Goal + ## Цель - * Create a useful overview of the results - * Check what has been done and summarize the results - * Communicate all the relevant results with the customer - * Identify the fundamental boundary conditions of the project + * Создать полезный обзор результатов + * Проверить, что было сделано, и обобщить результаты + * Сообщить все соответствующие результаты заказчику + * Определить основные граничные условия проекта - ## Description + ## Описание - * Each topic gets its own overview which will be used as a catalogue of results - * This overview informs all participants about the decisions made - * ... + * Каждая тема получает свой собственный обзор, который будет использоваться в качестве каталога результатов + * Этот обзор информирует всех участников о принятых решениях + * ... item_3: subject: Прохождение предварительного планирования description: Этот тип иерархически является родителем типов "Clash" и "Request", таким образом представляет общую заметку. @@ -289,17 +289,17 @@ ru: item_0: subject: Завершение дизайна description: |- - ## Goal + ## Цель - * Design is done - * All parties are happy with the results of the design planning phase + * Проектирование завершено + * Все стороны довольны результатами этапа планирования проектирования - ## Description + ## Описание - * The design of the project will be finished - * All parties agree on the design - * The owner is happy with the results - * ... + * Разработка проекта будет завершена + * Все стороны согласны с дизайном + * Владелец доволен результатами + * ... item_1: subject: Заморозка дизайна description: Этот тип является иерархически родителем типов "Clash" и "Request", таким образом представляет общую заметку. @@ -393,15 +393,15 @@ ru: item_6: subject: Вечеринка по случаю новоселья description: |- - ## Goal + ## Цель - * Have a blast! + * Повеселись! - ## Description + ## Описание - * Invite the construction team - * Invite your friends - * Bring some drinks, snacks and your smile + * Пригласите строительную команду + * Пригласите своих друзей + * Принесите напитки, закуски и свою улыбку demo-bim-project: name: "(Demo) Проект BIM" status_explanation: Все задачи и подпроекты выполняются по графику. Вовлеченные люди знают свои задачи. Система полностью настроена. @@ -474,32 +474,32 @@ ru: item_0: subject: Сбор специфичных для проекта данных и информации для BIM-модели description: |- - ## Goal + ## Цель - * Identify the information strategy for the customer (e.g. by using plain language questions) - * If provided, analyze the customer information requirements for the BIM model - * Define an information delivery strategy according to the customers needs + * Определите информационную стратегию для клиента (например, с помощью вопросов на простом языке) + * Если предусмотрено, проанализируйте требования к информации клиента для BIM-модели + * Определите стратегию предоставления информации в соответствии с потребностями клиентов - ## Description + ## Описание - * Analyzing the customers needs and goals for using the BIM methodology - * Results of this tasks should be: - * The requirements for the project - * A strategy for the delivery phase - * ... + * Анализ потребностей и целей клиентов для использования методологии BIM + * Результатами этих задач должны быть: + * Требования к проекту + * Стратегия для этапа доставки + * ... item_1: subject: Создание плана выполнения BIM description: |- - # Goal + # Цель - * A BIM execution plan will be defined according to the exchange requirements specifications (ERS) - * All team members and partners have a plan on how to reach each of the project goals + * План выполнения BIM будет определен согласно спецификациям требований (ERS) + * Все члены команды и партнеры имеют план достижения каждой из целей проекта - # Description + # Описание - * Depending on the identifies use cases, the individual Information Delivery Manuals will be defined - * To handle the technological interfaces, a software topology will be defined and analyzed and verified - * ... + * В зависимости от вариантов использования, индивидуальные Руководства по доставке информации будут определены + * Для обработки технологических интерфейсов, Программная топология будет определена и проанализирована и проверена + * ... item_2: subject: Завершение выполнения плана выполнения БИМ description: Этот тип иерархически является родителем типов "Clash" и "Request", таким образом представляет общую заметку. @@ -513,16 +513,16 @@ ru: item_0: subject: Моделирование исходной BIM-модели description: |- - # Goal + # Цель - * Modelling the initial BIM model - * Creating a BIM model for the whole project team + * Моделирование начальной модели BIM + * Создание модели BIM для всей команды проекта - # Description + # Описание - * According to the gathered data from the customer, the initial model will be modelled - * The model shall be modelled according to the LOD Matrices and contain the information needed - * ... + * По собранным данным от клиента, начальная модель будет смоделирована + * Модель должна быть смоделирована в соответствии с матрицами LOD и содержать необходимую информацию + * ... item_1: subject: Первоначальная внутренняя проверка и пересмотр модели description: |- diff --git a/modules/boards/app/components/boards/add_button_component.rb b/modules/boards/app/components/boards/add_button_component.rb new file mode 100644 index 00000000000..b07bfdd11bc --- /dev/null +++ b/modules/boards/app/components/boards/add_button_component.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ +# + +module Boards + class AddButtonComponent < ::AddButtonComponent + def render? + if current_project + User.current.allowed_to?(:manage_board_views, current_project) + else + User.current.allowed_to_globally?(:manage_board_views) + end + end + + def dynamic_path + polymorphic_path([:new, current_project, :work_package_board]) + end + + def id + 'add-board-button' + end + + def accessibility_label_text + I18n.t('boards.label_create_new_board') + end + + def label_text + I18n.t('boards.label_board') + end + end +end diff --git a/modules/boards/app/components/boards/row_component.rb b/modules/boards/app/components/boards/row_component.rb index 5256834460c..92e8d3bd05a 100644 --- a/modules/boards/app/components/boards/row_component.rb +++ b/modules/boards/app/components/boards/row_component.rb @@ -35,7 +35,7 @@ module Boards end def name - link_to model.name, project_work_package_boards_path(model.project, model) + link_to model.name, project_work_package_board_path(model.project, model) end def created_at @@ -50,5 +50,31 @@ module Boards t('boards.board_types.free') end end + + def button_links + [delete_link].compact + end + + def delete_link + if render_delete_link? + link_to( + '', + work_package_board_path(model), + method: :delete, + class: 'icon icon-delete', + data: { + confirm: I18n.t(:text_are_you_sure), + 'qa-selector': "board-remove-#{model.id}" + }, + title: t(:button_delete) + ) + end + end + + private + + def render_delete_link? + table.current_project && table.current_user.allowed_to?(:manage_board_views, table.current_project) + end end end diff --git a/modules/boards/app/components/boards/table_component.rb b/modules/boards/app/components/boards/table_component.rb index 90f202e0893..a73d9a7d596 100644 --- a/modules/boards/app/components/boards/table_component.rb +++ b/modules/boards/app/components/boards/table_component.rb @@ -30,7 +30,7 @@ module Boards class TableComponent < ::TableComponent - columns :name, :project_id, :type, :created_at + options :current_project, :current_user sortable_columns :name, :project_id, :created_at def initial_sort @@ -46,5 +46,14 @@ module Boards [attr, { caption: Boards::Grid.human_attribute_name(attr) }] end end + + def columns + @columns ||= [ + :name, + (:project_id if current_project.blank?), + :type, + :created_at + ].compact + end end end diff --git a/modules/boards/app/contracts/boards/create_contract.rb b/modules/boards/app/contracts/boards/create_contract.rb new file mode 100644 index 00000000000..9e5f1b7d95a --- /dev/null +++ b/modules/boards/app/contracts/boards/create_contract.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Boards + class CreateContract < ::Grids::CreateContract + private + + def validate_allowed + unless edit_allowed? + # "project" is what is exposed to the user in the global form, not "scope" + errors.add(:project, :blank) + end + end + end +end diff --git a/modules/boards/app/controllers/boards/boards_controller.rb b/modules/boards/app/controllers/boards/boards_controller.rb index d2f819043d4..976de7feaef 100644 --- a/modules/boards/app/controllers/boards/boards_controller.rb +++ b/modules/boards/app/controllers/boards/boards_controller.rb @@ -1,20 +1,23 @@ module ::Boards class BoardsController < BaseController before_action :find_optional_project + before_action :build_board_grid, only: %i[new] - with_options only: [:index] do + with_options only: %i[index show] do # The boards permission alone does not suffice # to view work packages before_action :authorize before_action :authorize_work_package_permission end - before_action :authorize_global, only: :overview + before_action :authorize_global, only: %i[overview new create destroy] + before_action :find_board_grid, only: %i[destroy] + before_action :ensure_board_type_not_restricted, only: %i[create] menu_item :board_view def index - render layout: 'angular/angular' + @board_grids = Boards::Grid.includes(:project).where(project: @project) end def overview @@ -31,12 +34,88 @@ module ::Boards :boards end + def show + render layout: 'angular/angular' + end + + def new; end + + def create + service_result = service_call + + @board_grid = service_result.result + + if service_result.success? + flash[:notice] = I18n.t(:notice_successful_create) + redirect_to project_work_package_board_path(@project, @board_grid) + else + @errors = service_result.errors + render action: :new + end + end + + def destroy + @board_grid.destroy + + flash[:notice] = I18n.t(:notice_successful_delete) + + respond_to do |format| + format.json do + render json: { redirect_url: project_work_package_boards_path(@project) } + end + format.html do + redirect_to action: 'index', project_id: @project + end + end + end + private + def find_board_grid + @board_grid = Boards::Grid.find(params[:id]) + @project = @board_grid.project + end + def authorize_work_package_permission unless current_user.allowed_to?(:view_work_packages, @project, global: @project.nil?) deny_access end end + + def build_board_grid + @board_grid = Boards::Grid.new + end + + def ensure_board_type_not_restricted + render_403 if restricted_board_type? + end + + def restricted_board_type? + !EnterpriseToken.allows_to?(:board_view) && board_grid_params[:attribute] != 'basic' + end + + def service_call + service_class.new(user: User.current) + .call( + project: @project, + name: board_grid_params[:name], + attribute: board_grid_params[:attribute] + ) + end + + def service_class + { + 'basic' => Boards::BasicBoardCreateService, + 'status' => Boards::StatusBoardCreateService, + 'assignee' => Boards::AssigneeBoardCreateService, + 'version' => Boards::VersionBoardCreateService, + 'subproject' => Boards::SubprojectBoardCreateService, + 'subtasks' => Boards::SubtasksBoardCreateService + }.fetch(board_grid_params[:attribute]) + end + + def board_grid_params + params.require(:boards_grid).permit(%i[name attribute]) + end end end diff --git a/modules/boards/app/helpers/boards_helper.rb b/modules/boards/app/helpers/boards_helper.rb new file mode 100644 index 00000000000..1e6d3669038 --- /dev/null +++ b/modules/boards/app/helpers/boards_helper.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module BoardsHelper + BoardTypeAttributes = Struct.new(:radio_button_value, + :title, + :description, + :image_path, + :disabled?) + + def board_types + [ + build_board_type_attributes('basic', 'lists', false), + build_board_type_attributes('status', 'status'), + build_board_type_attributes('assignee', 'assignees'), + build_board_type_attributes('version', 'version'), + build_board_type_attributes('subproject', 'subproject'), + build_board_type_attributes('subtasks', 'parent-child') + ] + end + + def build_board_type_attributes(type_name, image_name, disabled = !EnterpriseToken.allows_to?(:board_view)) + BoardTypeAttributes.new(type_name, + I18n.t("boards.board_type_attributes.#{type_name}"), + I18n.t("boards.board_type_descriptions.#{type_name}"), + "assets/images/board_creation_modal/#{image_name}.svg", + disabled) + end + + def global_board_create_context? + global_board_new_action? || global_board_create_action? + end + + def global_board_new_action? + request.path == new_work_package_board_path + end + + def global_board_create_action? + request.path == work_package_boards_path && @project.nil? + end +end diff --git a/modules/boards/app/services/boards/assignee_board_create_service.rb b/modules/boards/app/services/boards/assignee_board_create_service.rb new file mode 100644 index 00000000000..3083d6b86d9 --- /dev/null +++ b/modules/boards/app/services/boards/assignee_board_create_service.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Boards + class AssigneeBoardCreateService < BaseCreateService + private + + def no_widgets_initially? + true + end + end +end diff --git a/modules/boards/app/services/boards/base_create_service.rb b/modules/boards/app/services/boards/base_create_service.rb new file mode 100644 index 00000000000..d3538f94906 --- /dev/null +++ b/modules/boards/app/services/boards/base_create_service.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +module Boards + class BaseCreateService < ::Grids::CreateService + protected + + def instance(attributes) + Boards::Grid.new( + name: attributes[:name], + project: attributes[:project], + row_count: row_count_for_board, + column_count: column_count_for_board + ) + end + + def before_perform(params, _service_result) + return super(params, _service_result) if no_widgets_initially? + + create_query_result = create_query(params) + + return create_query_result if create_query_result.failure? + + super(params.merge(query_id: create_query_result.result.id), create_query_result) + end + + def set_attributes_params(params) + {}.tap do |grid_params| + grid_params[:options] = options_for_grid(params) + grid_params[:widgets] = options_for_widgets(params) + end + end + + def attributes_service_class + BaseSetAttributesService + end + + private + + def no_widgets_initially? + false + end + + def create_query(params) + Queries::CreateService.new(user: User.current) + .call(create_query_params(params)) + end + + def create_query_params(params) + default_create_query_params(params).merge( + name: query_name, + filters: query_filters + ) + end + + def default_create_query_params(params) + { + project: params[:project], + public: true, + sort_criteria: query_sort_criteria + } + end + + def query_name + raise 'Define the query name' + end + + def query_filters + raise 'Define the query filters' + end + + def query_sort_criteria + [[:manual_sorting, 'asc'], [:id, 'asc']] + end + + def options_for_grid(params) + {}.tap do |options| + if params[:attribute] == 'basic' + options[:type] = 'free' + else + options[:type] = 'action' + options[:attribute] = params[:attribute] + end + end + end + + def options_for_widgets(_params) + return [] if no_widgets_initially? + + raise 'Define the options for the grid widgets' + end + + def row_count_for_board + 1 + end + + def column_count_for_board + 4 + end + end +end diff --git a/modules/boards/app/services/boards/base_set_attributes_service.rb b/modules/boards/app/services/boards/base_set_attributes_service.rb new file mode 100644 index 00000000000..b625d791fa0 --- /dev/null +++ b/modules/boards/app/services/boards/base_set_attributes_service.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Boards + class BaseSetAttributesService < Grids::SetAttributesService + end +end diff --git a/modules/boards/app/services/boards/basic_board_create_service.rb b/modules/boards/app/services/boards/basic_board_create_service.rb new file mode 100644 index 00000000000..65705f1576e --- /dev/null +++ b/modules/boards/app/services/boards/basic_board_create_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Boards + class BasicBoardCreateService < BaseCreateService + private + + def query_name + 'Unnamed list' + end + + def query_filters + [{ manual_sort: { operator: 'ow', values: [] } }] + end + + def options_for_widgets(params) + [ + Grids::Widget.new( + start_row: 1, + start_column: 1, + end_row: 2, + end_column: 2, + identifier: "work_package_query", + options: { + "queryId" => params[:query_id], + "filters" => query_filters + } + ) + ] + end + end +end diff --git a/modules/boards/app/services/boards/status_board_create_service.rb b/modules/boards/app/services/boards/status_board_create_service.rb new file mode 100644 index 00000000000..500dfdd170c --- /dev/null +++ b/modules/boards/app/services/boards/status_board_create_service.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Boards + class StatusBoardCreateService < BaseCreateService + private + + def query_name + default_status.name + end + + def query_filters + [{ status_id: { operator: '=', values: [default_status.id.to_s] } }] + end + + def default_status + @default_status ||= ::Status.default + end + + def options_for_widgets(params) + [ + Grids::Widget.new( + start_row: 1, + start_column: 1, + end_row: 2, + end_column: 2, + identifier: "work_package_query", + options: { + "queryId" => params[:query_id], + "filters" => query_filters + } + ) + ] + end + end +end diff --git a/modules/boards/app/services/boards/subproject_board_create_service.rb b/modules/boards/app/services/boards/subproject_board_create_service.rb new file mode 100644 index 00000000000..e3bb27489c5 --- /dev/null +++ b/modules/boards/app/services/boards/subproject_board_create_service.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Boards + class SubprojectBoardCreateService < BaseCreateService + private + + def no_widgets_initially? + true + end + end +end diff --git a/modules/boards/app/services/boards/subtasks_board_create_service.rb b/modules/boards/app/services/boards/subtasks_board_create_service.rb new file mode 100644 index 00000000000..a3e00f67b78 --- /dev/null +++ b/modules/boards/app/services/boards/subtasks_board_create_service.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Boards + class SubtasksBoardCreateService < BaseCreateService + private + + def no_widgets_initially? + true + end + end +end diff --git a/modules/boards/app/services/boards/version_board_create_service.rb b/modules/boards/app/services/boards/version_board_create_service.rb new file mode 100644 index 00000000000..e689fdbfe72 --- /dev/null +++ b/modules/boards/app/services/boards/version_board_create_service.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Boards + class VersionBoardCreateService < BaseCreateService + protected + + def before_perform(params, _service_result) + create_queries_results = create_queries(params) + + return create_queries_results.find(&:failure?) if create_queries_results.any?(&:failure?) + + set_attributes(params.merge(query_ids: create_queries_results.map(&:result).map(&:id))) + end + + private + + def create_queries(params) + versions(params).map do |version| + Queries::CreateService.new(user: User.current) + .call(create_query_params(params, version)) + end + end + + def versions(params) + @versions ||= Version.includes(:project) + .where(projects: { id: params[:project].id }) + .with_status_open + end + + def create_query_params(params, version) + default_create_query_params(params).merge( + name: query_name(version), + filters: query_filters(version) + ) + end + + def query_name(version) + version.name + end + + def query_filters(version) + [{ version_id: { operator: '=', values: [version.id.to_s] } }] + end + + def options_for_widgets(params) + query_ids_with_versions = params[:query_ids].zip(versions(params)) + query_ids_with_versions.map.with_index do |(query_id, version), index| + Grids::Widget.new( + start_row: 1, + start_column: 1 + index, + end_row: 2, + end_column: 2 + index, + identifier: "work_package_query", + options: { + "queryId" => query_id, + "filters" => query_filters(version) + } + ) + end + end + end +end diff --git a/modules/boards/app/views/boards/boards/_form.html.erb b/modules/boards/app/views/boards/boards/_form.html.erb new file mode 100644 index 00000000000..dd3de65dde6 --- /dev/null +++ b/modules/boards/app/views/boards/boards/_form.html.erb @@ -0,0 +1,106 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + +<%= error_messages_for_contract @board_grid, @errors %> + +
    +
    + <%= f.text_field :name, + label: t(:label_title), + required: true, + size: 60, + container_class: '-wide' %> +
    + + <% if global_board_create_context? %> +
    + +
    + <%= angular_component_tag 'op-project-autocompleter', + inputs: { + apiFilters: [['user_action', '=', ['boards/create']]], + name: 'project_id', + value: @project, + appendTo: 'body', + }, + id: 'project_id', + class: 'form--select-container -wide remote-field--input', + data: { + 'qa-selector': 'project_id' + } + %> +
    +
    + <% end %> + +
    + +
    + <% unless EnterpriseToken.allows_to?(:board_view) %> +

    + <%= angular_component_tag 'op-enterprise-banner', + inputs: { + collapsible: false, + opReferrer: 'boards', + linkMessage: t('boards.upsale.upgrade'), + textMessage: t('boards.upsale.teaser_text'), + moreInfoLink: OpenProject::Static::Links.links[:enterprise_docs][:boards][:href] + } + %> +

    + <% end %> + +
    + <% board_types.each_with_index do |board_type, tile_number| %> + + <% end %> +
    +
    +
    +
    diff --git a/modules/boards/app/views/boards/boards/index.html.erb b/modules/boards/app/views/boards/boards/index.html.erb index d314de3539e..56b59b37eaa 100644 --- a/modules/boards/app/views/boards/boards/index.html.erb +++ b/modules/boards/app/views/boards/boards/index.html.erb @@ -1 +1,40 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + <% html_title(t('boards.label_boards')) -%> + +<%= toolbar title: t('boards.label_boards') do %> + <%= render Boards::AddButtonComponent.new(current_project: @project) %> +<% end %> + +<% if @board_grids.empty? -%> + <%= no_results_box %> +<% else -%> + <%= render Boards::TableComponent.new(rows: @board_grids, current_user: User.current, current_project: @project) %> +<% end -%> diff --git a/modules/boards/app/views/boards/boards/new.html.erb b/modules/boards/app/views/boards/boards/new.html.erb new file mode 100644 index 00000000000..921875a654a --- /dev/null +++ b/modules/boards/app/views/boards/boards/new.html.erb @@ -0,0 +1,37 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + +<% html_title t('boards.label_create_new_board') %> +<%= toolbar title: t('boards.label_create_new_board') %> +<%= labelled_tabular_form_for @board_grid, url: { controller: '/boards/boards', action: 'create', project_id: @project }, :html => {:id => 'board-form'} do |f| -%> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= styled_button_tag t(:button_create), class: '-highlight' %> + <%= link_to t(:button_cancel), { controller: 'boards/boards', action: (@project ? 'index' : 'overview') }, + class: 'button' %> +<% end %> diff --git a/modules/boards/app/views/boards/boards/overview.html.erb b/modules/boards/app/views/boards/boards/overview.html.erb index ae876b38d89..9d9a1934869 100644 --- a/modules/boards/app/views/boards/boards/overview.html.erb +++ b/modules/boards/app/views/boards/boards/overview.html.erb @@ -29,10 +29,12 @@ See COPYRIGHT and LICENSE files for more details. <% html_title(t('boards.label_boards')) -%> -<%= toolbar title: t('boards.label_boards') -%> +<%= toolbar title: t('boards.label_boards') do %> + <%= render Boards::AddButtonComponent.new %> +<% end %> <% if @board_grids.empty? -%> <%= no_results_box %> <% else -%> -<%= render Boards::TableComponent.new(rows: @board_grids) %> +<%= render Boards::TableComponent.new(rows: @board_grids, current_user: User.current) %> <% end -%> diff --git a/modules/boards/app/views/boards/boards/show.html.erb b/modules/boards/app/views/boards/boards/show.html.erb new file mode 100644 index 00000000000..3655423d098 --- /dev/null +++ b/modules/boards/app/views/boards/boards/show.html.erb @@ -0,0 +1,30 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + +<% html_title(t('boards.label_boards')) -%> diff --git a/modules/boards/config/locales/crowdin/af.yml b/modules/boards/config/locales/crowdin/af.yml index 876a434d21f..d5c7a472659 100644 --- a/modules/boards/config/locales/crowdin/af.yml +++ b/modules/boards/config/locales/crowdin/af.yml @@ -6,6 +6,8 @@ af: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ af: subproject: Sub-projek subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/ar.yml b/modules/boards/config/locales/crowdin/ar.yml index f451a91c40e..2669b9a075b 100644 --- a/modules/boards/config/locales/crowdin/ar.yml +++ b/modules/boards/config/locales/crowdin/ar.yml @@ -6,6 +6,8 @@ ar: boards: label_board: "لوحة" label_boards: "لوحات" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ ar: subproject: مشروع فرعي subtasks: الاصل والفرع basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/az.yml b/modules/boards/config/locales/crowdin/az.yml index 342462eba31..db9f9bcbcff 100644 --- a/modules/boards/config/locales/crowdin/az.yml +++ b/modules/boards/config/locales/crowdin/az.yml @@ -6,6 +6,8 @@ az: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ az: subproject: Subproject subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/be.yml b/modules/boards/config/locales/crowdin/be.yml index e8b96709fa8..56f30460882 100644 --- a/modules/boards/config/locales/crowdin/be.yml +++ b/modules/boards/config/locales/crowdin/be.yml @@ -6,6 +6,8 @@ be: boards: label_board: "Дошка" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ be: subproject: Subproject subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/bg.yml b/modules/boards/config/locales/crowdin/bg.yml index 9847982538c..fc83342e608 100644 --- a/modules/boards/config/locales/crowdin/bg.yml +++ b/modules/boards/config/locales/crowdin/bg.yml @@ -6,6 +6,8 @@ bg: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Основен action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ bg: subproject: Подпроект subtasks: Parent-child basic: Основен + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/ca.yml b/modules/boards/config/locales/crowdin/ca.yml index 4a5c49ac7b3..ec3d1330096 100644 --- a/modules/boards/config/locales/crowdin/ca.yml +++ b/modules/boards/config/locales/crowdin/ca.yml @@ -6,6 +6,8 @@ ca: boards: label_board: "Taulell" label_boards: "Taulells" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Bàsic action: "Panell d'acció (%{attribute})" @@ -16,3 +18,19 @@ ca: subproject: Subprojecte subtasks: Pare-fill basic: Bàsic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/ckb-IR.yml b/modules/boards/config/locales/crowdin/ckb-IR.yml index d50b3f4acbb..ed5676d29b0 100644 --- a/modules/boards/config/locales/crowdin/ckb-IR.yml +++ b/modules/boards/config/locales/crowdin/ckb-IR.yml @@ -6,6 +6,8 @@ ckb-IR: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ ckb-IR: subproject: Subproject subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/cs.yml b/modules/boards/config/locales/crowdin/cs.yml index 394558773c9..b367eb2a558 100644 --- a/modules/boards/config/locales/crowdin/cs.yml +++ b/modules/boards/config/locales/crowdin/cs.yml @@ -6,6 +6,8 @@ cs: boards: label_board: "Tabule" label_boards: "Tabule" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Základní action: "Akční tabule (%{attribute})" @@ -16,3 +18,19 @@ cs: subproject: Podprojekt subtasks: rodič-potomek basic: Základní + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgradovat nyní' diff --git a/modules/boards/config/locales/crowdin/da.yml b/modules/boards/config/locales/crowdin/da.yml index 1b61559eaa2..3e7c3b9223b 100644 --- a/modules/boards/config/locales/crowdin/da.yml +++ b/modules/boards/config/locales/crowdin/da.yml @@ -6,6 +6,8 @@ da: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Grundlæggende action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ da: subproject: Underprojekt subtasks: Parent-child basic: Grundlæggende + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/de.yml b/modules/boards/config/locales/crowdin/de.yml index 0b69e6b6f15..1d12c07c2ba 100644 --- a/modules/boards/config/locales/crowdin/de.yml +++ b/modules/boards/config/locales/crowdin/de.yml @@ -6,6 +6,8 @@ de: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Aktionsboard (%{attribute})" @@ -16,3 +18,19 @@ de: subproject: Unterprojekt subtasks: Eltern-Kind basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/el.yml b/modules/boards/config/locales/crowdin/el.yml index f718b4e58b9..40c4ce5e4b3 100644 --- a/modules/boards/config/locales/crowdin/el.yml +++ b/modules/boards/config/locales/crowdin/el.yml @@ -6,6 +6,8 @@ el: boards: label_board: "Πίνακας" label_boards: "Πίνακες" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Βασικό action: "Πίνακας ενεργειών (%{attribute})" @@ -16,3 +18,19 @@ el: subproject: Υποέργο subtasks: Parent-child basic: Βασικό + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/eo.yml b/modules/boards/config/locales/crowdin/eo.yml index ee92f6bc663..c993332e53e 100644 --- a/modules/boards/config/locales/crowdin/eo.yml +++ b/modules/boards/config/locales/crowdin/eo.yml @@ -6,6 +6,8 @@ eo: boards: label_board: "Tabulo" label_boards: "Tabuloj" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Agpanelo (%{attribute})" @@ -16,3 +18,19 @@ eo: subproject: Subprojekto subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/es.yml b/modules/boards/config/locales/crowdin/es.yml index 61a24f0caac..ea3b6c82a1d 100644 --- a/modules/boards/config/locales/crowdin/es.yml +++ b/modules/boards/config/locales/crowdin/es.yml @@ -6,6 +6,8 @@ es: boards: label_board: "Tablero" label_boards: "Tableros" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Básico action: "Tablero de acciones (%{attribute})" @@ -16,3 +18,19 @@ es: subproject: Subproyecto subtasks: Padre-hijo basic: Básico + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/et.yml b/modules/boards/config/locales/crowdin/et.yml index 486f5ff3334..b86657bc6a2 100644 --- a/modules/boards/config/locales/crowdin/et.yml +++ b/modules/boards/config/locales/crowdin/et.yml @@ -6,6 +6,8 @@ et: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ et: subproject: Alamprojekt subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/eu.yml b/modules/boards/config/locales/crowdin/eu.yml index 6a1c1836d20..a229be94ce7 100644 --- a/modules/boards/config/locales/crowdin/eu.yml +++ b/modules/boards/config/locales/crowdin/eu.yml @@ -6,6 +6,8 @@ eu: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ eu: subproject: Subproject subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/fa.yml b/modules/boards/config/locales/crowdin/fa.yml index 51952b1aedb..060acf71f09 100644 --- a/modules/boards/config/locales/crowdin/fa.yml +++ b/modules/boards/config/locales/crowdin/fa.yml @@ -6,6 +6,8 @@ fa: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ fa: subproject: زیر پروژه subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/fi.yml b/modules/boards/config/locales/crowdin/fi.yml index 3ff72f4cec9..c01c8f224d4 100644 --- a/modules/boards/config/locales/crowdin/fi.yml +++ b/modules/boards/config/locales/crowdin/fi.yml @@ -6,6 +6,8 @@ fi: boards: label_board: "Taulu" label_boards: "Taulut" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ fi: subproject: Aliprojekti subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/fil.yml b/modules/boards/config/locales/crowdin/fil.yml index 3ef2c41d71d..d2ddc126ad6 100644 --- a/modules/boards/config/locales/crowdin/fil.yml +++ b/modules/boards/config/locales/crowdin/fil.yml @@ -6,6 +6,8 @@ fil: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ fil: subproject: Kahaliling proyekto subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/fr.yml b/modules/boards/config/locales/crowdin/fr.yml index 4f95abfcf0a..d39a4c6984b 100644 --- a/modules/boards/config/locales/crowdin/fr.yml +++ b/modules/boards/config/locales/crowdin/fr.yml @@ -6,6 +6,8 @@ fr: boards: label_board: "Tableau" label_boards: "Tableaux" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Tableau d'action (%{attribute})" @@ -16,3 +18,19 @@ fr: subproject: Sous-projet subtasks: Parent-enfant basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/he.yml b/modules/boards/config/locales/crowdin/he.yml index c55d740468a..5124744f2d8 100644 --- a/modules/boards/config/locales/crowdin/he.yml +++ b/modules/boards/config/locales/crowdin/he.yml @@ -6,6 +6,8 @@ he: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ he: subproject: תת פרוייקט subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/hi.yml b/modules/boards/config/locales/crowdin/hi.yml index 75f16d43b51..93a36d48680 100644 --- a/modules/boards/config/locales/crowdin/hi.yml +++ b/modules/boards/config/locales/crowdin/hi.yml @@ -6,6 +6,8 @@ hi: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ hi: subproject: परियोजना की उपपरियोजना subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/hr.yml b/modules/boards/config/locales/crowdin/hr.yml index 7095698d41c..eec003136c4 100644 --- a/modules/boards/config/locales/crowdin/hr.yml +++ b/modules/boards/config/locales/crowdin/hr.yml @@ -6,6 +6,8 @@ hr: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ hr: subproject: Potprojekt subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/hu.yml b/modules/boards/config/locales/crowdin/hu.yml index d515385dde3..e037a53f036 100644 --- a/modules/boards/config/locales/crowdin/hu.yml +++ b/modules/boards/config/locales/crowdin/hu.yml @@ -6,6 +6,8 @@ hu: boards: label_board: "Tábla" label_boards: "Táblák" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Alap action: "Feladattábla" @@ -16,3 +18,19 @@ hu: subproject: Alprojekt subtasks: Szülő-gyerek basic: Alap + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/id.yml b/modules/boards/config/locales/crowdin/id.yml index 2336437d704..41bf85573ed 100644 --- a/modules/boards/config/locales/crowdin/id.yml +++ b/modules/boards/config/locales/crowdin/id.yml @@ -6,6 +6,8 @@ id: boards: label_board: "Papan" label_boards: "Papan" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Dasar action: "Papan aksi (%{attribute}" @@ -16,3 +18,19 @@ id: subproject: Sub-Project subtasks: Orangtua-anak basic: Dasar + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/it.yml b/modules/boards/config/locales/crowdin/it.yml index 9d9470e7e19..f872a64d8bf 100644 --- a/modules/boards/config/locales/crowdin/it.yml +++ b/modules/boards/config/locales/crowdin/it.yml @@ -6,6 +6,8 @@ it: boards: label_board: "Tabellone" label_boards: "Tabelloni" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Base action: "Scheda d'azione (%{attribute})" @@ -16,3 +18,19 @@ it: subproject: Sottoprogetto subtasks: genitore-figlio basic: Base + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/ja.yml b/modules/boards/config/locales/crowdin/ja.yml index 2192a79ab7b..b3ee66e3922 100644 --- a/modules/boards/config/locales/crowdin/ja.yml +++ b/modules/boards/config/locales/crowdin/ja.yml @@ -6,6 +6,8 @@ ja: boards: label_board: "ボード" label_boards: "ボード" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: 基本 action: "アクションボード (%{attribute})" @@ -16,3 +18,19 @@ ja: subproject: 子プロジェクト subtasks: 親子 basic: 基本 + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/ka.yml b/modules/boards/config/locales/crowdin/ka.yml index 0bd4862da97..6d65d69fded 100644 --- a/modules/boards/config/locales/crowdin/ka.yml +++ b/modules/boards/config/locales/crowdin/ka.yml @@ -6,6 +6,8 @@ ka: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: ძირითადი action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ ka: subproject: Subproject subtasks: Parent-child basic: ძირითადი + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/ko.yml b/modules/boards/config/locales/crowdin/ko.yml index 98ab862c395..02af7bc514f 100644 --- a/modules/boards/config/locales/crowdin/ko.yml +++ b/modules/boards/config/locales/crowdin/ko.yml @@ -6,6 +6,8 @@ ko: boards: label_board: "게시판" label_boards: "보드" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: 기본 action: "작업 보드(%{attribute})" @@ -16,3 +18,19 @@ ko: subproject: 하위 프로젝트 subtasks: 부모-자식 basic: 기본 + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/lt.yml b/modules/boards/config/locales/crowdin/lt.yml index faf04e61c61..d47aa81c030 100644 --- a/modules/boards/config/locales/crowdin/lt.yml +++ b/modules/boards/config/locales/crowdin/lt.yml @@ -6,6 +6,8 @@ lt: boards: label_board: "Lenta" label_boards: "Lentos" + label_create_new_board: "Kurti naują lentą" + label_board_type: "Lentos tipas" board_types: free: Pagrindinis action: "Veiksmų lenta (%{attribute})" @@ -16,3 +18,19 @@ lt: subproject: Sub-projektas subtasks: Tėvo-vaiko basic: Pagrindinis + board_type_descriptions: + basic: > + Pradėkite iš pradžių nuo tuščios lentos. Rankiniu būdu pridėkite korteles ir stulpelius į šią lentą. + status: > + Paprasta kanban stiliaus lenta su stulpeliais statusams, tokiems kaip To Do, Vykdoma, Įvykdyta. + assignee: > + Lenta su automatiniais stulpeliais pagrįstais priskirtu naudotoju. Ideali darbo paketų paskirstymui. + version: > + Lenta su automatiniais stulpeliais, pagrįstais versijos atributu. Ideali produkto vystymo planavimui. + subproject: > + Lenta su automatiniais stulpeliais sub-projektams. Darbų paketo perkėlimas į kitą sąrašą atitinkamai pakeičia (sub-)projektą. + subtasks: > + Lenta su automatiniais stulpeliais sub-elementams. Darbų paketo perkėlimas į kitą sąrašą atitinkamai atnaujina tėtį. + upsale: + teaser_text: 'Ar norite automatizuoti jūsų procesus su Lentomis? Išmaniosios lentos yra Enterprise priedas. Prašome pasikeisti į mokamą planą.' + upgrade: 'Užsisakykite dabar' diff --git a/modules/boards/config/locales/crowdin/lv.yml b/modules/boards/config/locales/crowdin/lv.yml index 51c54496f66..2a3eba54c6a 100644 --- a/modules/boards/config/locales/crowdin/lv.yml +++ b/modules/boards/config/locales/crowdin/lv.yml @@ -6,6 +6,8 @@ lv: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ lv: subproject: Subproject subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/mn.yml b/modules/boards/config/locales/crowdin/mn.yml index e167896e74c..7b331bde312 100644 --- a/modules/boards/config/locales/crowdin/mn.yml +++ b/modules/boards/config/locales/crowdin/mn.yml @@ -6,6 +6,8 @@ mn: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ mn: subproject: Subproject subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/ne.yml b/modules/boards/config/locales/crowdin/ne.yml index 1eb5eb0c5d8..c475ceb8ed2 100644 --- a/modules/boards/config/locales/crowdin/ne.yml +++ b/modules/boards/config/locales/crowdin/ne.yml @@ -6,6 +6,8 @@ ne: boards: label_board: "बोर्ड" label_boards: "बोर्डहरु" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ ne: subproject: Subproject subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/nl.yml b/modules/boards/config/locales/crowdin/nl.yml index 731030c538f..8969b6f9b3d 100644 --- a/modules/boards/config/locales/crowdin/nl.yml +++ b/modules/boards/config/locales/crowdin/nl.yml @@ -6,6 +6,8 @@ nl: boards: label_board: "Bord" label_boards: "Borden" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basis action: "Actiebord (%{attribute})" @@ -16,3 +18,19 @@ nl: subproject: Subproject subtasks: Ouder-kind basic: Basis + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/no.yml b/modules/boards/config/locales/crowdin/no.yml index 1087a93f6ea..cf0fa31530a 100644 --- a/modules/boards/config/locales/crowdin/no.yml +++ b/modules/boards/config/locales/crowdin/no.yml @@ -6,6 +6,8 @@ boards: label_board: "Tavle" label_boards: "Tavler" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Enkel action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ subproject: Underprosjekt subtasks: Parent-child basic: Enkel + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/pl.yml b/modules/boards/config/locales/crowdin/pl.yml index 9b621328471..0337805f8d5 100644 --- a/modules/boards/config/locales/crowdin/pl.yml +++ b/modules/boards/config/locales/crowdin/pl.yml @@ -6,6 +6,8 @@ pl: boards: label_board: "Tablica" label_boards: "Tablice" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Podstawowe action: "Tablica działania (%{attribute})" @@ -16,3 +18,19 @@ pl: subproject: Podprojekt subtasks: Nadrzędny-podrzędny basic: Podstawowe + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/pt.yml b/modules/boards/config/locales/crowdin/pt.yml index 27dd60a91cc..d7874fa8e8f 100644 --- a/modules/boards/config/locales/crowdin/pt.yml +++ b/modules/boards/config/locales/crowdin/pt.yml @@ -6,6 +6,8 @@ pt: boards: label_board: "Quadro" label_boards: "Quadros" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Básico action: "Quadro de ação (%{attribute})" @@ -16,3 +18,19 @@ pt: subproject: Subprojeto subtasks: Primeiro-secundário basic: Básico + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/ro.yml b/modules/boards/config/locales/crowdin/ro.yml index dd4a750341d..6e40409a71a 100644 --- a/modules/boards/config/locales/crowdin/ro.yml +++ b/modules/boards/config/locales/crowdin/ro.yml @@ -6,6 +6,8 @@ ro: boards: label_board: "Tablă" label_boards: "Panouri" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: De bază action: "Tablă de acțiune (%{attribute})" @@ -16,3 +18,19 @@ ro: subproject: Subproiect subtasks: Părinte-copil basic: De bază + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/ru.yml b/modules/boards/config/locales/crowdin/ru.yml index 6c443d07de6..56550d3c2fb 100644 --- a/modules/boards/config/locales/crowdin/ru.yml +++ b/modules/boards/config/locales/crowdin/ru.yml @@ -6,6 +6,8 @@ ru: boards: label_board: "Доска" label_boards: "Доски" + label_create_new_board: "Создать новую доску" + label_board_type: "Тип доски" board_types: free: Базовый action: "Доска действий (%{attribute})" @@ -16,3 +18,19 @@ ru: subproject: Подпроект subtasks: Родитель-Потомок basic: Базовый + board_type_descriptions: + basic: > + Начните с нуля с чистой доски. Вручную добавьте карточки и столбцы на эту доску. + status: > + Базовая доска в стиле канбан со столбцами для определения статуса, такими как "Сделано", "В процессе", "Готово". + assignee: > + Доска с автоматическими колонками на основе назначенных пользователей. Идеально подходит для отправки пакетов работ. + version: > + Доска с автоматизированными колонками на основе атрибута версии. Идеально подходит для планирования разработки продукта. + subproject: > + Доска с автоматизированными колонками для подпроектов. Перетаскивание пакетов работ в другие списки соответствующим образом обновляет подпроект. + subtasks: > + Доска с автоматизированными колонками для подэлементов. Перетаскивание пакетов работ в другие списки соответствующим образом обновляет родительский список. + upsale: + teaser_text: 'Хотите автоматизировать работу с досками? Обновите платный план до корпоративного.' + upgrade: 'Обновить сейчас' diff --git a/modules/boards/config/locales/crowdin/rw.yml b/modules/boards/config/locales/crowdin/rw.yml index e99acf5deef..8ae6d3036d1 100644 --- a/modules/boards/config/locales/crowdin/rw.yml +++ b/modules/boards/config/locales/crowdin/rw.yml @@ -6,6 +6,8 @@ rw: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ rw: subproject: Subproject subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/si.yml b/modules/boards/config/locales/crowdin/si.yml index cc541ddbc8c..8dde53ef9cf 100644 --- a/modules/boards/config/locales/crowdin/si.yml +++ b/modules/boards/config/locales/crowdin/si.yml @@ -6,6 +6,8 @@ si: boards: label_board: "මණ්ඩලය" label_boards: "පුවරු" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ si: subproject: උප ව්යාපෘතිය subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/sk.yml b/modules/boards/config/locales/crowdin/sk.yml index df61548949d..144bfafb57d 100644 --- a/modules/boards/config/locales/crowdin/sk.yml +++ b/modules/boards/config/locales/crowdin/sk.yml @@ -6,6 +6,8 @@ sk: boards: label_board: "Nástenka" label_boards: "Nástenky" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ sk: subproject: Podprojekt subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/sl.yml b/modules/boards/config/locales/crowdin/sl.yml index acbcd24f5a4..799c0c1fa02 100644 --- a/modules/boards/config/locales/crowdin/sl.yml +++ b/modules/boards/config/locales/crowdin/sl.yml @@ -6,6 +6,8 @@ sl: boards: label_board: "Tabla" label_boards: "Table" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Osnovno action: "Tabla opravil (%{attribute})" @@ -16,3 +18,19 @@ sl: subproject: Podprojekt subtasks: Starš-otrok basic: Osnovno + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/sr.yml b/modules/boards/config/locales/crowdin/sr.yml index b9c780c3b74..5e6a8a7166d 100644 --- a/modules/boards/config/locales/crowdin/sr.yml +++ b/modules/boards/config/locales/crowdin/sr.yml @@ -6,6 +6,8 @@ sr: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ sr: subproject: Subproject subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/sv.yml b/modules/boards/config/locales/crowdin/sv.yml index 0e8d4e0eed6..7a3f94e8cdf 100644 --- a/modules/boards/config/locales/crowdin/sv.yml +++ b/modules/boards/config/locales/crowdin/sv.yml @@ -6,6 +6,8 @@ sv: boards: label_board: "Tavla" label_boards: "Tavlor" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Enkel action: "Åtgärdstavla (%{attribute})" @@ -16,3 +18,19 @@ sv: subproject: Delprojekt subtasks: Parent-child basic: Enkel + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/th.yml b/modules/boards/config/locales/crowdin/th.yml index 1813ba0d8d3..0f1671c40f0 100644 --- a/modules/boards/config/locales/crowdin/th.yml +++ b/modules/boards/config/locales/crowdin/th.yml @@ -6,6 +6,8 @@ th: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ th: subproject: โครงการย่อย subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/tr.yml b/modules/boards/config/locales/crowdin/tr.yml index 9a1c3dcca76..7f7beeeddec 100644 --- a/modules/boards/config/locales/crowdin/tr.yml +++ b/modules/boards/config/locales/crowdin/tr.yml @@ -6,6 +6,8 @@ tr: boards: label_board: "Pano" label_boards: "Panolar" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Temel action: "Eylem kurulu (%{attribute})" @@ -16,3 +18,19 @@ tr: subproject: Alt proje subtasks: Ana-Yavru basic: Temel + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/uk.yml b/modules/boards/config/locales/crowdin/uk.yml index 8c90fdcc000..76da9a1d3a1 100644 --- a/modules/boards/config/locales/crowdin/uk.yml +++ b/modules/boards/config/locales/crowdin/uk.yml @@ -6,6 +6,8 @@ uk: boards: label_board: "Дошка" label_boards: "Дошки" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Базова action: "Дошка дій (%{attribute})" @@ -16,3 +18,19 @@ uk: subproject: Підпроєкт subtasks: Батьківські й дочірні елементи basic: Базова + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/vi.yml b/modules/boards/config/locales/crowdin/vi.yml index 152bef95432..adcf67e11f0 100644 --- a/modules/boards/config/locales/crowdin/vi.yml +++ b/modules/boards/config/locales/crowdin/vi.yml @@ -6,6 +6,8 @@ vi: boards: label_board: "Bảng" label_boards: "Bảng" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -16,3 +18,19 @@ vi: subproject: Dự án con subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/crowdin/zh-TW.yml b/modules/boards/config/locales/crowdin/zh-TW.yml index 402471b95ca..c55b920ee1a 100644 --- a/modules/boards/config/locales/crowdin/zh-TW.yml +++ b/modules/boards/config/locales/crowdin/zh-TW.yml @@ -6,6 +6,8 @@ zh-TW: boards: label_board: "面板" label_boards: "看板" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: 基本 action: "行動項看板 (%{attribute})" @@ -16,3 +18,19 @@ zh-TW: subproject: 子專案 subtasks: Parent-child basic: 基本 + board_type_descriptions: + basic: > + Start from scratch with a blank board. Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. Dragging work packages to other lists updates the parent accordingly. + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/locales/en.yml b/modules/boards/config/locales/en.yml index 893ca755275..e303e52bf32 100644 --- a/modules/boards/config/locales/en.yml +++ b/modules/boards/config/locales/en.yml @@ -7,6 +7,8 @@ en: boards: label_board: "Board" label_boards: "Boards" + label_create_new_board: "Create new board" + label_board_type: "Board type" board_types: free: Basic action: "Action board (%{attribute})" @@ -17,3 +19,25 @@ en: subproject: Subproject subtasks: Parent-child basic: Basic + board_type_descriptions: + basic: > + Start from scratch with a blank board. + Manually add cards and columns to this board. + status: > + Basic kanban style board with columns for status such as To Do, In Progress, Done. + assignee: > + Board with automated columns based on assigned users. + Ideal for dispatching work packages. + version: > + Board with automated columns based on the version attribute. + Ideal for planning product development. + subproject: > + Board with automated columns for subprojects. + Dragging work packages to other lists updates the (sub-)project accordingly. + subtasks: > + Board with automated columns for sub-elements. + Dragging work packages to other lists updates the parent accordingly. + + upsale: + teaser_text: 'Would you like to automate your workflows with Boards? Advanced boards are an Enterprise add-on. Please upgrade to a paid plan.' + upgrade: 'Upgrade now' diff --git a/modules/boards/config/routes.rb b/modules/boards/config/routes.rb index 8a930e909ea..89df20b5eff 100644 --- a/modules/boards/config/routes.rb +++ b/modules/boards/config/routes.rb @@ -1,11 +1,15 @@ OpenProject::Application.routes.draw do get '/boards/all', to: 'boards/boards#overview' - scope '', as: :work_package_boards do - get '/boards(/*state)', to: 'boards/boards#index' - end + resources :boards, + controller: 'boards/boards', + only: %i[index show new create destroy], + as: :work_package_boards scope 'projects/:project_id', as: 'project' do - get '/boards(/*state)', to: 'boards/boards#index', as: :work_package_boards + resources :boards, + controller: 'boards/boards', + only: %i[index show new create], + as: :work_package_boards end end diff --git a/modules/boards/lib/open_project/boards/engine.rb b/modules/boards/lib/open_project/boards/engine.rb index 5db32f429b2..016c0c97aa9 100644 --- a/modules/boards/lib/open_project/boards/engine.rb +++ b/modules/boards/lib/open_project/boards/engine.rb @@ -29,11 +29,11 @@ module OpenProject::Boards name: 'OpenProject Boards' do project_module :board_view, dependencies: :work_package_tracking, order: 80 do permission :show_board_views, - { 'boards/boards': %i[index overview] }, + { 'boards/boards': %i[index show overview] }, dependencies: :view_work_packages, contract_actions: { boards: %i[read] } permission :manage_board_views, - { 'boards/boards': %i[index] }, + { 'boards/boards': %i[index show new create destroy] }, dependencies: :manage_public_queries, contract_actions: { boards: %i[create update destroy] } end diff --git a/modules/boards/spec/contracts/boards/create_contract_spec.rb b/modules/boards/spec/contracts/boards/create_contract_spec.rb new file mode 100644 index 00000000000..33f0014e9ac --- /dev/null +++ b/modules/boards/spec/contracts/boards/create_contract_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' +require 'contracts/shared/model_contract_shared_context' + +RSpec.describe Boards::CreateContract do + include_context 'ModelContract shared context' + let(:project) { build_stubbed(:project) } + let(:name) { 'My Board' } + let(:user) { build_stubbed(:user) } + let(:grid) do + build_stubbed(:board_grid, project:, name:) + end + let(:contract) { described_class.new(grid, user) } + + context 'when :project not provided' do + let(:project) { nil } + + it_behaves_like 'contract is invalid', project: :blank + end + + context 'when :name not provided' do + let(:name) { nil } + + it_behaves_like 'contract is invalid', name: :blank + end +end diff --git a/modules/boards/spec/features/action_boards/assignee_board_spec.rb b/modules/boards/spec/features/action_boards/assignee_board_spec.rb index 0ea6828f7e2..439227ed677 100644 --- a/modules/boards/spec/features/action_boards/assignee_board_spec.rb +++ b/modules/boards/spec/features/action_boards/assignee_board_spec.rb @@ -92,7 +92,9 @@ RSpec.describe 'Assignee action board', board_index.visit! # Create new board - board_page = board_index.create_board action: :Assignee, expect_empty: true + board_page = board_index.create_board title: 'My Assignee Board', + action: 'Assignee', + expect_empty: true # Expect no assignees to be present board_page.expect_empty @@ -113,7 +115,7 @@ RSpec.describe 'Assignee action board', board_page.expect_list_option '(none)' board_page.board(reload: true) do |board| - expect(board.name).to eq 'Action board (assignee)' + expect(board.name).to eq 'My Assignee Board' queries = board.contained_queries expect(queries.count).to eq(3) diff --git a/modules/boards/spec/features/action_boards/custom_field_filters_spec.rb b/modules/boards/spec/features/action_boards/custom_field_filters_spec.rb index c14bb1ef0c6..1b4e236bce8 100644 --- a/modules/boards/spec/features/action_boards/custom_field_filters_spec.rb +++ b/modules/boards/spec/features/action_boards/custom_field_filters_spec.rb @@ -92,7 +92,7 @@ RSpec.describe 'Custom field filter in boards', js: true, with_ee: %i[board_view board_index.visit! # Create new board - board_page = board_index.create_board action: :Status + board_page = board_index.create_board action: 'Status' # expect lists of default status board_page.expect_list 'Open' @@ -103,7 +103,7 @@ RSpec.describe 'Custom field filter in boards', js: true, with_ee: %i[board_view filters.add_filter_by(custom_field.name, 'is (OR)', - ['A', 'B'], + %w[A B], custom_field.attribute_name(:camel_case)) board_page.expect_changed diff --git a/modules/boards/spec/features/action_boards/status_board_spec.rb b/modules/boards/spec/features/action_boards/status_board_spec.rb index 36a6feec7f4..ed018e58abd 100644 --- a/modules/boards/spec/features/action_boards/status_board_spec.rb +++ b/modules/boards/spec/features/action_boards/status_board_spec.rb @@ -94,7 +94,7 @@ RSpec.describe 'Status action board', js: true, with_ee: %i[board_view] do board_index.visit! # Create new board - board_page = board_index.create_board action: :Status + board_page = board_index.create_board action: 'Status' # expect lists of default status board_page.expect_list 'Open' @@ -111,7 +111,7 @@ RSpec.describe 'Status action board', js: true, with_ee: %i[board_view] do board_index.visit! # Create new board - board_page = board_index.create_board action: :Status + board_page = board_index.create_board action: 'Status' board_page.add_list option: 'Whatever' board_page.expect_list 'Whatever' @@ -152,7 +152,8 @@ RSpec.describe 'Status action board', js: true, with_ee: %i[board_view] do board_index.visit! # Create new board - board_page = board_index.create_board action: :Status + board_page = board_index.create_board title: 'My Status Board', + action: 'Status' # expect lists of default status board_page.expect_list 'Open' @@ -161,7 +162,7 @@ RSpec.describe 'Status action board', js: true, with_ee: %i[board_view] do board_page.expect_list 'Closed' board_page.board(reload: true) do |board| - expect(board.name).to eq 'Action board (status)' + expect(board.name).to eq 'My Status Board' queries = board.contained_queries expect(queries.count).to eq(2) @@ -298,14 +299,15 @@ RSpec.describe 'Status action board', js: true, with_ee: %i[board_view] do board_index.visit! # Create new board - board_page = board_index.create_board action: :Status + board_page = board_index.create_board action: 'Status' # expect lists of default status board_page.expect_list 'Open' expect(board_page.list_count).to eq(1) + board_index.visit! # Create another status board - second_board_page = board_index.create_board action: :Status, via_toolbar: true + second_board_page = board_index.create_board action: 'Status', via_toolbar: false # Expect only one list with the default status second_board_page.expect_list 'Open' diff --git a/modules/boards/spec/features/action_boards/status_type_moving_board_spec.rb b/modules/boards/spec/features/action_boards/status_type_moving_board_spec.rb index 239d9552f4e..e6b9fa3d327 100644 --- a/modules/boards/spec/features/action_boards/status_type_moving_board_spec.rb +++ b/modules/boards/spec/features/action_boards/status_type_moving_board_spec.rb @@ -111,7 +111,7 @@ RSpec.describe 'Status action board', js: true, with_ee: %i[board_view] do board_index.visit! # Create new board - board_page = board_index.create_board action: :Status + board_page = board_index.create_board action: 'Status' # expect lists of default status board_page.expect_list 'Open' diff --git a/modules/boards/spec/features/action_boards/subproject_board_spec.rb b/modules/boards/spec/features/action_boards/subproject_board_spec.rb index 9afa89bc00f..8b38e17b1b5 100644 --- a/modules/boards/spec/features/action_boards/subproject_board_spec.rb +++ b/modules/boards/spec/features/action_boards/subproject_board_spec.rb @@ -82,7 +82,7 @@ RSpec.describe 'Subproject action board', js: true, with_ee: %i[board_view] do board_index.visit! # Create new board - board_page = board_index.create_board action: :Subproject, expect_empty: true + board_page = board_index.create_board action: 'Subproject', expect_empty: true # Expect we can add a child 1 board_page.add_list option: 'Child 1' @@ -111,7 +111,9 @@ RSpec.describe 'Subproject action board', js: true, with_ee: %i[board_view] do board_index.visit! # Create new board - board_page = board_index.create_board action: :Subproject, expect_empty: true + board_page = board_index.create_board title: 'My Subproject Board', + action: 'Subproject', + expect_empty: true # Expect we can add a child 1 board_page.add_list option: 'Child 1' @@ -124,7 +126,7 @@ RSpec.describe 'Subproject action board', js: true, with_ee: %i[board_view] do board_page.expect_movable 'Child 1', 'Foo', movable: true board_page.board(reload: true) do |board| - expect(board.name).to eq 'Action board (subproject)' + expect(board.name).to eq 'My Subproject Board' queries = board.contained_queries expect(queries.count).to eq(1) diff --git a/modules/boards/spec/features/action_boards/subtasks_board_spec.rb b/modules/boards/spec/features/action_boards/subtasks_board_spec.rb index 34296bee7ee..fbc089a682c 100644 --- a/modules/boards/spec/features/action_boards/subtasks_board_spec.rb +++ b/modules/boards/spec/features/action_boards/subtasks_board_spec.rb @@ -27,8 +27,8 @@ #++ require 'spec_helper' -require_relative './../support//board_index_page' -require_relative './../support/board_page' +require_relative '../support//board_index_page' +require_relative '../support/board_page' RSpec.describe 'Subtasks action board', js: true, with_ee: %i[board_view] do let(:type) { create(:type_standard) } @@ -62,7 +62,7 @@ RSpec.describe 'Subtasks action board', js: true, with_ee: %i[board_view] do board_index.visit! # Create new board - board_page = board_index.create_board action: :Parent_child, expect_empty: true + board_page = board_index.create_board action: 'Parent-child', expect_empty: true # Expect we can add a work package column board_page.add_list option: 'Parent WP' @@ -86,7 +86,9 @@ RSpec.describe 'Subtasks action board', js: true, with_ee: %i[board_view] do board_index.visit! # Create new board - board_page = board_index.create_board action: :Parent_child, expect_empty: true + board_page = board_index.create_board title: 'My Parent-child Board', + action: 'Parent-child', + expect_empty: true # Expect we can add a child 1 board_page.add_list option: 'Parent WP' @@ -99,7 +101,7 @@ RSpec.describe 'Subtasks action board', js: true, with_ee: %i[board_view] do board_page.expect_movable 'Parent WP', 'Child', movable: true board_page.board(reload: true) do |board| - expect(board.name).to eq 'Action board (parent-child)' + expect(board.name).to eq 'My Parent-child Board' queries = board.contained_queries expect(queries.count).to eq(1) @@ -157,7 +159,7 @@ RSpec.describe 'Subtasks action board', js: true, with_ee: %i[board_view] do it 'prevents adding a work package to its own column' do board_index.visit! - board_page = board_index.create_board action: :Parent_child, expect_empty: true + board_page = board_index.create_board action: 'Parent-child', expect_empty: true board_page.add_list option: 'Parent WP' board_page.expect_list 'Parent WP' board_page.expect_card 'Parent WP', 'Child' diff --git a/modules/boards/spec/features/action_boards/version_board_spec.rb b/modules/boards/spec/features/action_boards/version_board_spec.rb index 25d3ac3227b..90186a811c4 100644 --- a/modules/boards/spec/features/action_boards/version_board_spec.rb +++ b/modules/boards/spec/features/action_boards/version_board_spec.rb @@ -75,7 +75,8 @@ RSpec.describe 'Version action board', js: true, with_ee: %i[board_view] do board_index.visit! # Create new board - board_page = board_index.create_board action: :Version + board_page = board_index.create_board title: 'My Version Board', + action: 'Version' # expect lists of open versions board_page.expect_list 'Open version' @@ -96,7 +97,7 @@ RSpec.describe 'Version action board', js: true, with_ee: %i[board_view] do login_as(user) end - it 'allows management of boards' do + fit 'allows management of boards' do board_page = create_new_version_board board_page.expect_card 'Open version', work_package.subject, present: true @@ -105,7 +106,7 @@ RSpec.describe 'Version action board', js: true, with_ee: %i[board_view] do board_page.expect_list_option 'Closed version' board_page.board(reload: true) do |board| - expect(board.name).to eq 'Action board (version)' + expect(board.name).to eq 'My Version Board' queries = board.contained_queries expect(queries.count).to eq(2) diff --git a/modules/boards/spec/features/board_conflicts_spec.rb b/modules/boards/spec/features/board_conflicts_spec.rb index e8d1ceed5c0..10eb979f9ba 100644 --- a/modules/boards/spec/features/board_conflicts_spec.rb +++ b/modules/boards/spec/features/board_conflicts_spec.rb @@ -61,7 +61,7 @@ RSpec.describe 'Board remote changes resolution', js: true, with_ee: %i[board_vi board_index.visit! # Create new board - board_page = board_index.create_board action: :Status + board_page = board_index.create_board action: 'Status' # expect lists of default status board_page.expect_list 'Open' diff --git a/modules/boards/spec/features/board_enterprise_spec.rb b/modules/boards/spec/features/board_enterprise_spec.rb index 08a816ba008..9a768d09546 100644 --- a/modules/boards/spec/features/board_enterprise_spec.rb +++ b/modules/boards/spec/features/board_enterprise_spec.rb @@ -27,8 +27,8 @@ #++ require 'spec_helper' -require_relative './support/board_index_page' -require_relative './support/board_page' +require_relative 'support/board_index_page' +require_relative 'support/board_page' RSpec.describe 'Boards enterprise spec', :js, :with_cuprite do shared_let(:admin) { create(:admin) } @@ -56,8 +56,8 @@ RSpec.describe 'Boards enterprise spec', :js, :with_cuprite do it 'disabled all action boards' do page.find('.toolbar-item a', text: 'Board').click - expect(page).to have_selector('[data-qa-selector="op-tile-block"]:not([disabled])', text: 'Basic') - expect(page).to have_selector('[data-qa-selector="op-tile-block"]:disabled', count: 5) + expect(page).to have_selector('[data-qa-selector="op-tile-block"]:not(.-disabled)', text: 'Basic') + expect(page).to have_selector('[data-qa-selector="op-tile-block"].-disabled', count: 5) end it 'shows a banner on the action board' do @@ -85,7 +85,7 @@ RSpec.describe 'Boards enterprise spec', :js, :with_cuprite do it 'enables all options' do page.find('.toolbar-item a', text: 'Board').click - expect(page).to have_selector('[data-qa-selector="op-tile-block"]:not([disabled])', count: 6) + expect(page).to have_selector('[data-qa-selector="op-tile-block"]:not(.-disabled)', count: 6) end it 'shows the action board' do diff --git a/modules/boards/spec/features/board_highlighting_spec.rb b/modules/boards/spec/features/board_highlighting_spec.rb index 40eaa63b0c5..126cc14b345 100644 --- a/modules/boards/spec/features/board_highlighting_spec.rb +++ b/modules/boards/spec/features/board_highlighting_spec.rb @@ -74,7 +74,7 @@ RSpec.describe 'Work Package boards spec', js: true, with_ee: %i[board_view] do it 'navigates from boards to the WP full view and back' do board_index.visit! - board_page = board_index.create_board action: :Status + board_page = board_index.create_board action: 'Status' # See the work packages board_page.expect_query 'Open', editable: false diff --git a/modules/boards/spec/features/board_index_spec.rb b/modules/boards/spec/features/board_index_spec.rb new file mode 100644 index 00000000000..bf9c8490263 --- /dev/null +++ b/modules/boards/spec/features/board_index_spec.rb @@ -0,0 +1,142 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' +require_relative 'support/board_index_page' + +RSpec.describe 'Work Package Project Boards Index Page', + :js, + :with_cuprite, + with_ee: %i[board_view], + with_flag: { more_global_index_pages: true } do + # The identifier is important to test https://community.openproject.com/wp/29754 + shared_let(:project) { create(:project, identifier: 'boards', enabled_module_names: %i[work_package_tracking board_view]) } + + shared_let(:management_permissions) do + %i[show_board_views manage_board_views add_work_packages view_work_packages manage_public_queries] + end + shared_let(:view_only_permissions) do + %i[show_board_views add_work_packages view_work_packages] + end + + shared_let(:priority) { create(:default_priority) } + shared_let(:status) { create(:default_status) } + + let(:user) do + create(:user, + member_in_project: project, + member_through_role: role) + end + let(:permissions) { management_permissions } + let(:role) { create(:role, permissions:) } + + let(:board_view) { create(:board_grid_with_query, name: 'My board', project:) } + let(:other_board_view) { create(:board_grid_with_query, name: 'My other board', project:) } + + let(:board_index) { Pages::BoardIndex.new(project) } + + before do + login_as user + end + + context 'as a user with board management permissions' do + let(:permissions) { management_permissions } + + it 'shows a create button' do + board_index.visit! + + board_index.expect_create_button + end + end + + context 'as a user without board management permissions' do + let(:permissions) { view_only_permissions } + + it 'does not show a create button' do + board_index.visit! + + board_index.expect_no_create_button + end + end + + context 'when no boards exist' do + it 'displays the empty message' do + board_index.visit! + + board_index.expect_no_boards_listed + end + end + + context 'when boards exist' do + before do + board_view + other_board_view + end + + it 'lists the boards' do + board_index.visit! + + board_index.expect_boards_listed(board_view, other_board_view) + end + + context 'as a user with board management permissions' do + let(:permissions) { management_permissions } + + it 'renders delete links for each board' do + board_index.visit! + + board_index.expect_delete_button(board_view) + board_index.expect_delete_button(other_board_view) + end + end + + context 'as a user without board management permissions' do + let(:permissions) { view_only_permissions } + + it 'does not render delete links' do + board_index.visit! + + board_index.expect_no_delete_button(board_view) + board_index.expect_no_delete_button(other_board_view) + end + end + + it 'paginates results', with_settings: { per_page_options: '1' } do + # First page displays the historically last meeting + board_index.visit! + board_index.expect_boards_listed(board_view) + board_index.expect_boards_not_listed(other_board_view) + + board_index.expect_to_be_on_page(1) + + board_index.to_page(2) + board_index.expect_boards_listed(other_board_view) + board_index.expect_boards_not_listed(board_view) + end + end +end diff --git a/modules/boards/spec/features/board_management_spec.rb b/modules/boards/spec/features/board_management_spec.rb index c32b2d30292..7321e2792bd 100644 --- a/modules/boards/spec/features/board_management_spec.rb +++ b/modules/boards/spec/features/board_management_spec.rb @@ -116,7 +116,7 @@ RSpec.describe 'Board management spec', js: true, with_ee: %i[board_view] do board_index.expect_board board_view.name # Create new board - board_page = board_index.create_board action: nil + board_page = board_index.create_board board_page.rename_board 'Board test' # Rename through toolbar @@ -203,6 +203,19 @@ RSpec.describe 'Board management spec', js: true, with_ee: %i[board_view] do board_page.delete_board board_index.expect_board 'Board foo', present: false end + + it 'allows creating a new project board form via the sidebar' do + board_index.visit! + + board_page = board_index.create_board title: 'My Board created via the sidebar', + via_toolbar: false + board_page.board(reload: true) do |board| + expect(board.name).to eq 'My Board created via the sidebar' + queries = board.contained_queries + expect(queries.count).to eq(1) + expect(queries.first.name).to eq 'Unnamed list' + end + end end context 'with view boards + work package permission' do diff --git a/modules/boards/spec/features/board_navigation_spec.rb b/modules/boards/spec/features/board_navigation_spec.rb index 8a3f21e7de9..62cd270e6b4 100644 --- a/modules/boards/spec/features/board_navigation_spec.rb +++ b/modules/boards/spec/features/board_navigation_spec.rb @@ -74,7 +74,7 @@ RSpec.describe 'Work Package boards spec', js: true, with_ee: %i[board_view] do # Click back goes back to the board find('.work-packages-back-button').click - expect(page).to have_current_path project_work_package_boards_path(project, board_view.id) + expect(page).to have_current_path project_work_package_board_path(project, board_view) # Open the details page with the info icon card = board_page.card_for(wp) @@ -98,14 +98,14 @@ RSpec.describe 'Work Package boards spec', js: true, with_ee: %i[board_view] do end it 'navigates correctly the path from overview page to the boards page' do - visit "/projects/#{project.identifier}" + visit project_path(project) item = page.find('#menu-sidebar li[data-name="board_view"]', wait: 10) item.find('.toggler').click subitem = page.find('[data-qa-selector="op-sidemenu--item-action--Myboard"]', wait: 10) # Ends with boards due to lazy route - expect(subitem[:href]).to end_with "/projects/#{project.identifier}/boards" + expect(subitem[:href]).to end_with project_work_package_boards_path(project) subitem.click @@ -128,6 +128,7 @@ RSpec.describe 'Work Package boards spec', js: true, with_ee: %i[board_view] do # Open the details page with the info icon card = board_page.card_for(wp) split_view = card.open_details_view + split_view.ensure_page_loaded split_view.expect_subject split_view.switch_to_tab tab: 'Relations' diff --git a/modules/boards/spec/features/board_overview_spec.rb b/modules/boards/spec/features/board_overview_spec.rb index 60111488f78..41e40409b52 100644 --- a/modules/boards/spec/features/board_overview_spec.rb +++ b/modules/boards/spec/features/board_overview_spec.rb @@ -29,27 +29,38 @@ require 'spec_helper' require_relative './support/board_overview_page' -RSpec.describe 'Work Package boards overview spec', - with_cuprite: true, +RSpec.describe 'Work Package Boards Overview', + :with_cuprite, with_ee: %i[board_view], with_flag: { more_global_index_pages: true } do + # The identifier is important to test https://community.openproject.com/wp/29754 + shared_let(:project) { create(:project, identifier: 'boards', enabled_module_names: %i[work_package_tracking board_view]) } + shared_let(:other_project) { create(:project, enabled_module_names: %i[work_package_tracking board_view]) } + + shared_let(:management_permissions) do + %i[show_board_views manage_board_views add_work_packages view_work_packages manage_public_queries] + end + shared_let(:view_only_permissions) do + %i[show_board_views add_work_packages view_work_packages] + end + + shared_let(:priority) { create(:default_priority) } + shared_let(:status) { create(:default_status) } + let(:user) do create(:user, member_in_project: project, member_through_role: role) end - # The identifier is important to test https://community.openproject.com/wp/29754 - let(:project) { create(:project, identifier: 'boards', enabled_module_names: %i[work_package_tracking board_view]) } - let(:other_project) { create(:project, enabled_module_names: %i[work_package_tracking board_view]) } - let(:permissions) { %i[show_board_views manage_board_views add_work_packages view_work_packages manage_public_queries] } + let(:permissions) { management_permissions } let(:role) { create(:role, permissions:) } - let!(:priority) { create(:default_priority) } - let!(:status) { create(:default_status) } - let(:board_overview) { Pages::BoardOverview.new } + let(:board_view) { create(:board_grid_with_query, name: 'My board', project:) } let(:other_board_view) { create(:board_grid_with_query, name: 'My other board', project:) } let(:other_project_board_view) { create(:board_grid_with_query, name: 'Unseeable Board', project: other_project) } + let(:board_overview) { Pages::BoardOverview.new } + before do login_as user end @@ -60,6 +71,26 @@ RSpec.describe 'Work Package boards overview spec', board_overview.expect_global_menu_item_selected end + context 'as a user with board management permissions' do + let(:permissions) { management_permissions } + + it 'shows a create button' do + board_overview.visit! + + board_overview.expect_create_button + end + end + + context 'as a user without board management permissions' do + let(:permissions) { view_only_permissions } + + it 'does not show a create button' do + board_overview.visit! + + board_overview.expect_no_create_button + end + end + context 'when no boards exist' do it 'displays the empty message' do board_overview.visit! @@ -80,7 +111,7 @@ RSpec.describe 'Work Package boards overview spec', end end - context 'when boards exists' do + context 'when boards exist' do before do board_view other_board_view @@ -94,6 +125,14 @@ RSpec.describe 'Work Package boards overview spec', board_overview.expect_boards_not_listed(other_project_board_view) end + it 'does not render delete links' do + board_overview.visit! + + board_overview.expect_no_delete_button(board_view) + board_overview.expect_no_delete_button(other_board_view) + board_overview.expect_no_delete_button(other_project_board_view) + end + it 'paginates results', with_settings: { per_page_options: '1' } do # First page displays the historically last meeting board_overview.visit! diff --git a/modules/boards/spec/features/board_reference_work_package_spec.rb b/modules/boards/spec/features/board_reference_work_package_spec.rb index ecb427a7695..415d873f6f1 100644 --- a/modules/boards/spec/features/board_reference_work_package_spec.rb +++ b/modules/boards/spec/features/board_reference_work_package_spec.rb @@ -70,7 +70,7 @@ RSpec.describe 'Board reference work package spec', js: true, with_ee: %i[board_ board_index.visit! # Create new board - board_page = board_index.create_board action: nil + board_page = board_index.create_board board_page.rename_list 'Unnamed list', 'First' # Filter for Version @@ -108,7 +108,7 @@ RSpec.describe 'Board reference work package spec', js: true, with_ee: %i[board_ board_index.visit! # Create new board - board_page = board_index.create_board action: nil + board_page = board_index.create_board board_page.rename_list 'Unnamed list', 'First' # Reference an existing work package diff --git a/modules/boards/spec/features/boards_global_create_spec.rb b/modules/boards/spec/features/boards_global_create_spec.rb new file mode 100644 index 00000000000..3fc270b9462 --- /dev/null +++ b/modules/boards/spec/features/boards_global_create_spec.rb @@ -0,0 +1,218 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_relative 'support/board_new_page' + +RSpec.describe 'Boards', + 'Creating a view from a Global Context', + :js, + :with_cuprite, + with_ee: %i[board_view] do + shared_let(:project) { create(:project, enabled_module_names: %i[work_package_tracking board_view]) } + shared_let(:other_project) { create(:project, enabled_module_names: %i[work_package_tracking board_view]) } + shared_let(:admin) { create(:admin) } + + shared_let(:status) { create(:default_status) } + shared_let(:versions) { create_list(:version, 3, project:) } + shared_let(:excluded_versions) do + [ + create(:version, project:, status: 'closed'), + create(:version, project: other_project, sharing: 'system') + ] + end + + shared_let(:new_board_page) { Pages::NewBoard.new } + + before do + login_as admin + end + + context 'within the global index page' do + before do + visit boards_all_path + end + + context 'when clicking on the create button' do + before do + new_board_page.navigate_by_create_button + end + + it 'navigates to the global create form' do + expect(page).to have_current_path new_work_package_board_path + expect(page).to have_content I18n.t('boards.label_create_new_board') + end + end + end + + context 'within the global create page' do + before do + new_board_page.visit! + end + + context 'with a Community Edition', with_ee: %i[] do + it 'renders an enterprise banner and disables all restriced board types', :aggregate_failures do + expect(page).to have_selector('op-enterprise-banner') + expect(page).to have_selector(:radio_button, 'Basic') + + %w[Status Assignee Version Subproject Parent-child].each do |restricted_board_type| + expect(page).to have_selector(:radio_button, restricted_board_type, disabled: true) + end + end + end + + context 'with an Enterprise Edition' do + context 'with all fields set' do + before do + wait_for_reload # Halt until the project autocompleter is ready + + new_board_page.set_title "Gotham Renewal Board" + new_board_page.set_project project + end + + context 'when creating a "Basic" board' do + before do + new_board_page.set_board_type 'Basic' + new_board_page.click_on_submit + + wait_for_reload + end + + it 'creates the board and redirects me to it' do + expect(page).to have_text(I18n.t(:notice_successful_create)) + expect(page).to have_current_path("/projects/#{project.identifier}/boards/#{Boards::Grid.last.id}") + expect(page).to have_text "Gotham Renewal Board" + end + end + + context 'when creating a "Status" board' do + before do + new_board_page.set_board_type 'Status' + new_board_page.click_on_submit + + wait_for_reload + end + + it 'creates the board and redirects me to it' do + expect(page).to have_text(I18n.t(:notice_successful_create)) + expect(page).to have_current_path("/projects/#{project.identifier}/boards/#{Boards::Grid.last.id}") + expect(page).to have_text "Gotham Renewal Board" + expect(page).to have_selector("[data-query-name='#{status.name}']") + end + end + + context 'when creating an "Assignee" board' do + before do + new_board_page.set_board_type 'Assignee' + new_board_page.click_on_submit + + wait_for_reload + end + + it 'creates the board and redirects me to it' do + expect(page).to have_text(I18n.t(:notice_successful_create)) + expect(page).to have_current_path("/projects/#{project.identifier}/boards/#{Boards::Grid.last.id}") + expect(page).to have_text "Gotham Renewal Board" + end + end + + context 'when creating a "Version" board' do + before do + new_board_page.set_board_type 'Version' + new_board_page.click_on_submit + + wait_for_reload + end + + it 'creates the board and redirects me to it', :aggregate_failures do + expect(page).to have_text(I18n.t(:notice_successful_create)) + expect(page).to have_current_path("/projects/#{project.identifier}/boards/#{Boards::Grid.last.id}") + expect(page).to have_text "Gotham Renewal Board" + versions.each do |version| + expect(page).to have_selector("[data-query-name='#{version.name}'") + end + excluded_versions.each do |version| + expect(page).not_to have_selector("[data-query-name='#{version.name}'") + end + end + end + + context 'when creating a "Subproject" board' do + before do + new_board_page.set_board_type 'Subproject' + new_board_page.click_on_submit + + wait_for_reload + end + + it 'creates the board and redirects me to it' do + expect(page).to have_text(I18n.t(:notice_successful_create)) + expect(page).to have_current_path("/projects/#{project.identifier}/boards/#{Boards::Grid.last.id}") + expect(page).to have_text "Gotham Renewal Board" + end + end + + context 'when creating a "Parent-child" board' do + before do + new_board_page.set_board_type 'Parent-child' + new_board_page.click_on_submit + + wait_for_reload + end + + it 'creates the board and redirects me to it' do + expect(page).to have_text(I18n.t(:notice_successful_create)) + expect(page).to have_current_path("/projects/#{project.identifier}/boards/#{Boards::Grid.last.id}") + expect(page).to have_text "Gotham Renewal Board" + end + end + end + + context 'when missing a required field' do + describe 'title' do + before do + wait_for_reload # Halt until the project autocompleter is ready + + new_board_page.set_project(project) + new_board_page.click_on_submit + end + + it 'renders a required attribute validation error' do + expect(Boards::Grid.all).to be_empty + + # Required HTML attribute just warns + expect(page).to have_current_path(new_work_package_board_path) + end + end + + describe 'project_id' do + before do + new_board_page.set_title("Batman's Itinerary") + new_board_page.click_on_submit + + wait_for_reload + end + + it 'renders a required attribute validation error' do + expect(Boards::Grid.all).to be_empty + + new_board_page.expect_toast message: "Project #{I18n.t('activerecord.errors.messages.blank')}", + type: :error + new_board_page.expect_project_dropdown + end + end + end + end + + describe 'cancel button' do + context "when it's clicked" do + before do + new_board_page.click_on_cancel_button + end + + it 'navigates back to the global index page' do + expect(page).to have_current_path(boards_all_path) + end + end + end + end +end diff --git a/modules/boards/spec/features/boards_sorting_spec.rb b/modules/boards/spec/features/boards_sorting_spec.rb index 13b0f2d39be..74416b989c2 100644 --- a/modules/boards/spec/features/boards_sorting_spec.rb +++ b/modules/boards/spec/features/boards_sorting_spec.rb @@ -27,8 +27,8 @@ #++ require 'spec_helper' -require_relative './support/board_index_page' -require_relative './support/board_page' +require_relative 'support/board_index_page' +require_relative 'support/board_page' RSpec.describe 'Work Package boards sorting spec', js: true, with_ee: %i[board_view] do let(:admin) { create(:admin) } @@ -47,38 +47,27 @@ RSpec.describe 'Work Package boards sorting spec', js: true, with_ee: %i[board_v # By adding each board the sort of table will change # The currently added board should be at the top it 'sorts the boards grid and menu based on their names' do - board_page = board_index.create_board action: nil + board_page = board_index.create_board title: 'My Basic Board' - retry_block do - board_page.back_to_index - find('[data-qa-selector="boards-table-column--name"]', text: 'Unnamed board') - end + board_page.back_to_index + board_index.expect_boards_listed 'My Basic Board' + query_menu.expect_menu_entry 'My Basic Board' - expect(page.all('[data-qa-selector="boards-table-column--name"]').map(&:text)) - .to eq ['Unnamed board'] - query_menu.expect_menu_entry 'Unnamed board' + board_page = board_index.create_board title: 'My Action Board', + action: 'Version', + expect_empty: true + board_page.back_to_index + board_index.expect_boards_listed 'My Action Board', + 'My Basic Board' + query_menu.expect_menu_entry 'My Action Board' - board_page = board_index.create_board action: :Version, expect_empty: true - retry_block do - board_page.back_to_index - find('[data-qa-selector="boards-table-column--name"]', text: 'Action board (version)') - end - - expect(page.all('[data-qa-selector="boards-table-column--name"]').map(&:text)) - .to eq ['Action board (version)', 'Unnamed board'] - query_menu.expect_menu_entry 'Action board (version)' - - board_page = board_index.create_board action: :Status + board_page = board_index.create_board title: 'My Status Board', + action: 'Status' board_page.back_to_index - retry_block do - board_page.back_to_index - find('[data-qa-selector="boards-table-column--name"]', text: 'Action board (status)') - end - - expect(page.all('[data-qa-selector="boards-table-column--name"]').map(&:text)) - .to eq ['Action board (status)', 'Action board (version)', 'Unnamed board'] - - query_menu.expect_menu_entry 'Action board (status)' + board_index.expect_boards_listed 'My Status Board', + 'My Action Board', + 'My Basic Board' + query_menu.expect_menu_entry 'My Status Board' end end diff --git a/modules/boards/spec/features/support/board_index_page.rb b/modules/boards/spec/features/support/board_index_page.rb index cb26f4b5377..d962ab27fb7 100644 --- a/modules/boards/spec/features/support/board_index_page.rb +++ b/modules/boards/spec/features/support/board_index_page.rb @@ -27,10 +27,11 @@ #++ require 'support/pages/page' -require_relative './board_page' +require_relative 'board_list_page' +require_relative 'board_new_page' module Pages - class BoardIndex < Page + class BoardIndex < BoardListPage attr_reader :project def initialize(project = nil) @@ -56,15 +57,20 @@ module Pages expect(page).to have_conditional_selector(present, 'td.name', text: name) end - def create_board(action: nil, expect_empty: false, via_toolbar: false) + def create_board(action: 'Basic', title: "#{action} Board", expect_empty: false, via_toolbar: true) if via_toolbar - page.find('[data-qa-selector="sidebar--create-board-button"]').click + within '.toolbar-items' do + click_link 'Board' + end else - page.find('.toolbar-item a', text: 'Board').click + find('[data-qa-selector="sidebar--create-board-button"]').click end - text = action == nil ? 'Basic' : action.to_s[0..5] - find('[data-qa-selector="op-tile-block-title"]', text:).click + new_board_page = NewBoard.new + + new_board_page.set_title title + new_board_page.set_board_type action + new_board_page.click_on_submit if expect_empty expect(page).to have_selector('.boards-list--add-item-text', wait: 10) diff --git a/modules/boards/spec/features/support/board_list_page.rb b/modules/boards/spec/features/support/board_list_page.rb new file mode 100644 index 00000000000..70267f6526f --- /dev/null +++ b/modules/boards/spec/features/support/board_list_page.rb @@ -0,0 +1,105 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'support/pages/page' + +module Pages + class BoardListPage < Page + def visit! + raise 'Define how to visit me' + end + + def expect_create_button + within '.toolbar-items' do + expect(page).to have_link 'Board' + end + end + + def expect_no_create_button + within '.toolbar-items' do + expect(page).not_to have_link 'Board' + end + end + + def expect_delete_button(board) + within '#content-wrapper' do + expect(page).to have_selector "[data-qa-selector='board-remove-#{board.id}']" + end + end + + def expect_no_delete_button(board) + within '#content-wrapper' do + expect(page).not_to have_selector "[data-qa-selector='board-remove-#{board.id}']" + end + end + + def expect_boards_listed(*boards) + board_names = if boards.all? { |board| board.to_s == board } + boards + else + boards.map(&:name) + end + + within '#content-wrapper' do + board_names.each do |board_name| + expect(page).to have_selector("td.name", text: board_name) + end + end + end + + def expect_boards_not_listed(*boards) + board_names = if boards.all? { |board| board.to_s == board } + boards + else + boards.map(&:name) + end + + within '#content-wrapper' do + board_names.each do |board_name| + expect(page).not_to have_selector("td.title", text: board_name) + end + end + end + + def expect_no_boards_listed + within '#content-wrapper' do + expect(page).to have_content I18n.t(:no_results_title_text) + end + end + + def expect_to_be_on_page(number) + expect(page).to have_selector('.op-pagination--item_current', text: number) + end + + def to_page(number) + within '.op-pagination--pages' do + click_link number.to_s + end + end + end +end diff --git a/modules/boards/spec/features/support/board_new_page.rb b/modules/boards/spec/features/support/board_new_page.rb new file mode 100644 index 00000000000..6ad950171a4 --- /dev/null +++ b/modules/boards/spec/features/support/board_new_page.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'support/pages/page' + +module Pages + class NewBoard < Page + include ::Components::Autocompleter::NgSelectAutocompleteHelpers + + def visit! + visit new_work_package_board_path + end + + def navigate_by_create_button + visit boards_all_path unless page.current_path == boards_all_path + + within '.toolbar-items' do + click_link 'Board' + end + end + + def set_title(title) + fill_in I18n.t(:label_title), with: title + end + + def expect_project_dropdown + find "[data-qa-selector='project_id']" + end + + def set_project(project) + select_autocomplete(find('[data-qa-selector="project_id"]'), + query: project, + results_selector: 'body', + wait_for_fetched_options: false) + end + + def set_board_type(board_type) + choose board_type, match: :first + end + + def click_on_submit + click_on I18n.t(:button_create) + end + + def click_on_cancel_button + click_on 'Cancel' + end + end +end diff --git a/modules/boards/spec/features/support/board_overview_page.rb b/modules/boards/spec/features/support/board_overview_page.rb index 793c115d545..cac8cfa9c0a 100644 --- a/modules/boards/spec/features/support/board_overview_page.rb +++ b/modules/boards/spec/features/support/board_overview_page.rb @@ -27,10 +27,10 @@ #++ require 'support/pages/page' -require_relative './board_page' +require_relative 'board_list_page' module Pages - class BoardOverview < Page + class BoardOverview < BoardListPage def visit! navigate_to_modules_menu_item("Boards") end @@ -40,37 +40,5 @@ module Pages expect(page).to have_selector('.selected', text: 'Boards') end end - - def expect_no_boards_listed - within '#content-wrapper' do - expect(page).to have_content I18n.t(:no_results_title_text) - end - end - - def expect_boards_listed(*boards) - within '#content-wrapper' do - boards.each do |board| - expect(page).to have_selector("td.name", text: board.name) - end - end - end - - def expect_to_be_on_page(number) - expect(page).to have_selector('.op-pagination--item_current', text: number) - end - - def to_page(number) - within '.op-pagination--pages' do - click_link number.to_s - end - end - - def expect_boards_not_listed(*boards) - within '#content-wrapper' do - boards.each do |board| - expect(page).not_to have_selector("td.title", text: board.name) - end - end - end end end diff --git a/modules/boards/spec/features/support/board_page.rb b/modules/boards/spec/features/support/board_page.rb index 4f5ae76875e..ee0a261a08e 100644 --- a/modules/boards/spec/features/support/board_page.rb +++ b/modules/boards/spec/features/support/board_page.rb @@ -264,9 +264,9 @@ module Pages def visit! if board.project - visit project_work_package_boards_path(project_id: board.project.id, state: board.id) + visit project_work_package_board_path(board.project, board) else - visit work_package_boards_path(state: board.id) + visit work_package_board_path(board) end end diff --git a/modules/boards/spec/routing/boards_routing_spec.rb b/modules/boards/spec/routing/boards_routing_spec.rb index 9a938ad8c7d..f4954bbbcf0 100644 --- a/modules/boards/spec/routing/boards_routing_spec.rb +++ b/modules/boards/spec/routing/boards_routing_spec.rb @@ -29,15 +29,51 @@ require 'spec_helper' RSpec.describe 'Boards routing' do - it { + it do expect(subject) - .to route(:get, '/projects/foobar/boards/state') - .to(controller: 'boards/boards', action: 'index', project_id: 'foobar', state: 'state') - } + .to route(:get, '/boards/all') + .to(controller: 'boards/boards', action: 'overview') + end - it { + it do expect(subject) - .to route(:get, '/boards/state') - .to(controller: 'boards/boards', action: 'index', state: 'state') - } + .to route(:get, '/boards') + .to(controller: 'boards/boards', action: 'index') + end + + it do + expect(subject) + .to route(:get, '/boards/1') + .to(controller: 'boards/boards', action: 'show', id: 1) + end + + it do + expect(subject) + .to route(:get, '/projects/foobar/boards/1') + .to(controller: 'boards/boards', action: 'show', project_id: 'foobar', id: 1) + end + + it do + expect(subject) + .to route(:get, '/boards/new') + .to(controller: 'boards/boards', action: 'new') + end + + it do + expect(subject) + .to route(:get, '/projects/foobar/boards/new') + .to(controller: 'boards/boards', action: 'new', project_id: 'foobar') + end + + it do + expect(subject) + .to route(:post, '/projects/foobar/boards') + .to(controller: 'boards/boards', action: 'create', project_id: 'foobar') + end + + it do + expect(subject) + .to route(:post, '/boards') + .to(controller: 'boards/boards', action: 'create') + end end diff --git a/modules/storages/spec/services/storages/project_storages/shared_synchronization_trigger_examples.rb b/modules/boards/spec/services/assignee_board_create_service_spec.rb similarity index 60% rename from modules/storages/spec/services/storages/project_storages/shared_synchronization_trigger_examples.rb rename to modules/boards/spec/services/assignee_board_create_service_spec.rb index 21ec1b43956..0b9d6ef2503 100644 --- a/modules/storages/spec/services/storages/project_storages/shared_synchronization_trigger_examples.rb +++ b/modules/boards/spec/services/assignee_board_create_service_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) 2012-2023 the OpenProject GmbH @@ -26,29 +28,36 @@ # See COPYRIGHT and LICENSE files for more details. #++ -RSpec.shared_examples 'a nextcloud synchronization trigger' do - context 'when project_folder mode is automatic' do - it 'schedules appropriate background job' do - model_instance.project_folder_mode = 'automatic' - subject - expect(enqueued_jobs.count).to eq(1) - expect(enqueued_jobs[0][:job]).to eq(Storages::ManageNextcloudIntegrationEventsJob) - end - end +require 'spec_helper' - context 'when project_folder mode is manual' do - it 'does not schedule a background job' do - model_instance.project_folder_mode = 'manual' - subject - expect(enqueued_jobs.count).to eq(0) - end - end +RSpec.describe Boards::AssigneeBoardCreateService do + shared_let(:project) { create(:project) } + shared_let(:user) { build_stubbed(:admin) } + shared_let(:instance) { described_class.new(user:) } - context 'when project_folder mode is inactive' do - it 'does not schedule a background job' do - model_instance.project_folder_mode = 'inactive' - subject - expect(enqueued_jobs.count).to eq(0) + subject { instance.call(params) } + + context 'with all valid params' do + let(:params) do + { + name: "Gotham Renewal Board", + project:, + attribute: 'assignee' + } + end + + it 'is successful' do + expect(subject).to be_success + end + + it 'creates an "Assignee" board with no widgets attached', :aggregate_failures do + board = subject.result + + expect(board.name).to eq("Gotham Renewal Board") + expect(board.options[:attribute]).to eq('assignee') + expect(board.options[:type]).to eq('action') + + expect(board.widgets).to be_empty end end end diff --git a/modules/meeting/lib/open_project/meeting/patches/textile_converter_patch.rb b/modules/boards/spec/services/base_create_service_shared_examples.rb similarity index 78% rename from modules/meeting/lib/open_project/meeting/patches/textile_converter_patch.rb rename to modules/boards/spec/services/base_create_service_shared_examples.rb index 2b68eac8a6a..6e9b91e57af 100644 --- a/modules/meeting/lib/open_project/meeting/patches/textile_converter_patch.rb +++ b/modules/boards/spec/services/base_create_service_shared_examples.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) 2012-2023 the OpenProject GmbH @@ -26,21 +28,14 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module OpenProject::Meeting::Patches - module TextileConverterPatch - extend ActiveSupport::Concern +require 'spec_helper' - included do - prepend(Patch) - end +RSpec.shared_examples 'sets the appropriate sort_criteria on each query' do + it '', :aggregate_failures do + subject - module Patch - def models_to_convert - super.merge( - ::MeetingContent => [:text], - ::Journal::MeetingContentJournal => [:text] - ) - end - end + queries_sort_criteria = queries.map(&:sort_criteria) + + expect(queries_sort_criteria).to all eq([%w[manual_sorting asc], %w[id asc]]) end end diff --git a/modules/boards/spec/services/basic_board_create_service_spec.rb b/modules/boards/spec/services/basic_board_create_service_spec.rb new file mode 100644 index 00000000000..5666e0a6f45 --- /dev/null +++ b/modules/boards/spec/services/basic_board_create_service_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' +require_relative 'base_create_service_shared_examples' + +RSpec.describe Boards::BasicBoardCreateService do + shared_let(:project) { create(:project) } + shared_let(:user) { build_stubbed(:admin) } + shared_let(:instance) { described_class.new(user:) } + + subject { instance.call(params) } + + context 'with all valid params' do + let(:params) do + { + name: "Gotham Renewal Board", + project:, + attribute: 'basic' + } + end + + it 'is successful' do + expect(subject).to be_success + end + + it 'creates a "Basic" board', :aggregate_failures do + board = subject.result + + expect(board.name).to eq("Gotham Renewal Board") + expect(board.options[:attribute]).to be_nil + expect(board.options[:type]).to eq 'free' + end + + describe 'widgets and queries' do + let(:board) { subject.result } + let(:widgets) { board.widgets } + let(:queries) { Query.all } + + it 'creates one of each', :aggregate_failures do + subject + + expect(widgets.count).to eq 1 + expect(queries.count).to eq 1 + end + + it 'sets the manual sorting filter on each' do + subject + + query_filter = queries.flat_map(&:filters).map(&:to_hash).first + widget_filter = widgets.flat_map { _1.options['filters'] }.first + + expect(query_filter).to have_key :manual_sort + expect(query_filter).to eq widget_filter + end + + it_behaves_like 'sets the appropriate sort_criteria on each query' + end + end +end diff --git a/modules/boards/spec/services/status_board_create_service_spec.rb b/modules/boards/spec/services/status_board_create_service_spec.rb new file mode 100644 index 00000000000..b585eb6dca9 --- /dev/null +++ b/modules/boards/spec/services/status_board_create_service_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' +require_relative 'base_create_service_shared_examples' + +RSpec.describe Boards::StatusBoardCreateService do + shared_let(:project) { create(:project) } + shared_let(:status) { create(:default_status) } + shared_let(:user) { build_stubbed(:admin) } + shared_let(:instance) { described_class.new(user:) } + + subject { instance.call(params) } + + context 'with all valid params' do + let(:params) do + { + name: "Gotham Renewal Board", + project:, + attribute: 'status' + } + end + + it 'is successful' do + expect(subject).to be_success + end + + it 'creates a "Status" board', :aggregate_failures do + board = subject.result + + expect(board.name).to eq("Gotham Renewal Board") + expect(board.options[:attribute]).to eq('status') + expect(board.options[:type]).to eq('action') + end + + describe 'widgets and queries' do + let(:board) { subject.result } + let(:widgets) { board.widgets } + let(:queries) { Query.all } + + it 'creates one of each for the current default status', :aggregate_failures do + subject + + expect(widgets.count).to eq 1 + expect(queries.count).to eq 1 + end + + it 'sets the filters on each' do + subject + + query_filter = queries.flat_map(&:filters).map(&:to_hash).first + widget_filter = widgets.flat_map { _1.options['filters'] }.first + + expect(query_filter).to match_array(widget_filter) + end + + it_behaves_like 'sets the appropriate sort_criteria on each query' + end + end +end diff --git a/modules/boards/spec/services/subproject_board_create_service_spec.rb b/modules/boards/spec/services/subproject_board_create_service_spec.rb new file mode 100644 index 00000000000..0c9837d83f7 --- /dev/null +++ b/modules/boards/spec/services/subproject_board_create_service_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' + +RSpec.describe Boards::SubprojectBoardCreateService do + shared_let(:project) { create(:project) } + shared_let(:user) { build_stubbed(:admin) } + shared_let(:instance) { described_class.new(user:) } + + subject { instance.call(params) } + + context 'with all valid params' do + let(:params) do + { + name: "Gotham Renewal Board", + project:, + attribute: 'subproject' + } + end + + it 'is successful' do + expect(subject).to be_success + end + + it 'creates a "Subproject" board with no widgets attached', :aggregate_failures do + board = subject.result + + expect(board.name).to eq("Gotham Renewal Board") + expect(board.options[:attribute]).to eq('subproject') + expect(board.options[:type]).to eq('action') + + expect(board.widgets).to be_empty + end + end +end diff --git a/modules/boards/spec/services/subtasks_board_create_service_spec.rb b/modules/boards/spec/services/subtasks_board_create_service_spec.rb new file mode 100644 index 00000000000..0dedb208138 --- /dev/null +++ b/modules/boards/spec/services/subtasks_board_create_service_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' + +RSpec.describe Boards::SubtasksBoardCreateService do + shared_let(:project) { create(:project) } + shared_let(:user) { build_stubbed(:admin) } + shared_let(:instance) { described_class.new(user:) } + + subject { instance.call(params) } + + context 'with all valid params' do + let(:params) do + { + name: "Gotham Renewal Board", + project:, + attribute: 'subtasks' + } + end + + it 'is successful' do + expect(subject).to be_success + end + + it 'creates a "Parent-child" board with no widgets attached', :aggregate_failures do + board = subject.result + + expect(board.name).to eq("Gotham Renewal Board") + expect(board.options[:attribute]).to eq('subtasks') + expect(board.options[:type]).to eq('action') + + expect(board.widgets).to be_empty + end + end +end diff --git a/modules/boards/spec/services/version_board_create_service_spec.rb b/modules/boards/spec/services/version_board_create_service_spec.rb new file mode 100644 index 00000000000..7579668e288 --- /dev/null +++ b/modules/boards/spec/services/version_board_create_service_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' +require_relative 'base_create_service_shared_examples' + +RSpec.describe Boards::VersionBoardCreateService do + shared_let(:project) { create(:project) } + shared_let(:other_project) { create(:project) } + shared_let(:versions) { create_list(:version, 3, project:) } + shared_let(:excluded_versions) do + [ + create(:version, project:, status: 'closed'), + create(:version, project: other_project, sharing: 'system') + ] + end + + shared_let(:user) { build_stubbed(:admin) } + shared_let(:instance) { described_class.new(user:) } + + subject { instance.call(params) } + + context 'with all valid params' do + let(:params) do + { + name: "Gotham Renewal Board", + project:, + attribute: 'version' + } + end + + it 'is successful' do + expect(subject).to be_success + end + + it 'creates a "Version" board', :aggregate_failures do + board = subject.result + + expect(board.name).to eq("Gotham Renewal Board") + expect(board.options[:attribute]).to eq('version') + expect(board.options[:type]).to eq('action') + end + + describe 'widgets and queries' do + let(:board) { subject.result } + let(:widgets) { board.widgets } + let(:queries) { Query.all } + + it 'creates one of each per expected version', :aggregate_failures do + subject + + expect(widgets.count).to eq(versions.count) + expect(queries.count).to eq(versions.count) + + expect(queries.map(&:name)).to match_array(versions.map(&:name)) + end + + it 'sets the filters on each' do + subject + + queries_filters = queries.flat_map(&:filters).map(&:to_hash) + widgets_filters = widgets.flat_map { _1.options['filters'] } + + expect(queries_filters).to match_array(widgets_filters) + end + + it_behaves_like 'sets the appropriate sort_criteria on each query' + end + end +end diff --git a/modules/calendar/app/services/calendar/create_ical_service.rb b/modules/calendar/app/services/calendar/create_ical_service.rb index 5d542ed1690..8fef8864a54 100644 --- a/modules/calendar/app/services/calendar/create_ical_service.rb +++ b/modules/calendar/app/services/calendar/create_ical_service.rb @@ -166,7 +166,7 @@ module Calendar replace_newlines: false ) - "\n#{WorkPackage.human_attribute_name(:description)}:\n #{stripped_text}" + "\n#{WorkPackage.human_attribute_name(:description)}:\n#{stripped_text}" end end end diff --git a/modules/calendar/config/locales/crowdin/de.yml b/modules/calendar/config/locales/crowdin/de.yml index e7bde975812..a2fd48cda00 100644 --- a/modules/calendar/config/locales/crowdin/de.yml +++ b/modules/calendar/config/locales/crowdin/de.yml @@ -2,7 +2,7 @@ de: label_calendar: "Kalender" label_calendar_plural: "Kalender" - label_new_calendar: "New calendar" + label_new_calendar: "Neuer Kalender" permission_view_calendar: "Kalender ansehen" permission_manage_calendars: "Kalender verwalten" permission_share_calendars: "iCalendar abonnieren" diff --git a/modules/calendar/config/locales/crowdin/nl.yml b/modules/calendar/config/locales/crowdin/nl.yml index c68d24bf574..a65d6f78579 100644 --- a/modules/calendar/config/locales/crowdin/nl.yml +++ b/modules/calendar/config/locales/crowdin/nl.yml @@ -2,8 +2,8 @@ nl: label_calendar: "Kalender" label_calendar_plural: "Agenda's" - label_new_calendar: "New calendar" + label_new_calendar: "Nieuwe kalender" permission_view_calendar: "Bekijk agenda's" permission_manage_calendars: "Agenda's beheren" - permission_share_calendars: "Subscribe to iCalendars" + permission_share_calendars: "Abonneren op iCalendars" project_module_calendar_view: "Agenda's" diff --git a/modules/calendar/spec/features/calendar_sharing_spec.rb b/modules/calendar/spec/features/calendar_sharing_spec.rb index 70e4816511c..4afc37d68ac 100644 --- a/modules/calendar/spec/features/calendar_sharing_spec.rb +++ b/modules/calendar/spec/features/calendar_sharing_spec.rb @@ -27,7 +27,7 @@ #++ require 'spec_helper' -require_relative './shared_context' +require_relative 'shared_context' RSpec.describe 'Calendar sharing via ical', js: true do include_context 'with calendar full access' @@ -45,6 +45,12 @@ RSpec.describe 'Calendar sharing via ical', js: true do share_calendars ]) end + let(:saved_query) do + create(:query_with_view_work_packages_calendar, + user: user_with_sharing_permission, + project:, + public: false) + end let(:user_without_sharing_permission) do # missing share_calendars permission @@ -66,13 +72,6 @@ RSpec.describe 'Calendar sharing via ical', js: true do member_in_project: project) end - let(:saved_query) do - create(:query_with_view_work_packages_calendar, - user: user_with_sharing_permission, - project:, - public: false) - end - context 'without sufficient permissions and the ical_enabled setting enabled', with_settings: { ical_enabled: true } do let(:saved_query) do create(:query_with_view_work_packages_calendar, @@ -108,12 +107,12 @@ RSpec.describe 'Calendar sharing via ical', js: true do # expect disabled sharing menu item within "#settingsDropdown" do - # expect(page).to have_button("Subscribe to iCalendar", disabled: true) # disabled selector not working - expect(page).to have_selector(".menu-item.inactive", text: "Subscribe to iCalendar") - page.click_button("Subscribe to iCalendar") + # expect(page).to have_button("Subscribe to calendar", disabled: true) # disabled selector not working + expect(page).to have_selector(".menu-item.inactive", text: "Subscribe to calendar") + page.click_button("Subscribe to calendar") # modal should not be shown - expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to iCalendar") + expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to calendar") end end end @@ -155,12 +154,12 @@ RSpec.describe 'Calendar sharing via ical', js: true do # expect disabled sharing menu item within "#settingsDropdown" do - # expect(page).to have_button("Subscribe to iCalendar", disabled: true) # disabled selector not working - expect(page).to have_selector(".menu-item.inactive", text: "Subscribe to iCalendar") - page.click_button("Subscribe to iCalendar") + # expect(page).to have_button("Subscribe to calendar", disabled: true) # disabled selector not working + expect(page).to have_selector(".menu-item.inactive", text: "Subscribe to calendar") + page.click_button("Subscribe to calendar") # modal should not be shown - expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to iCalendar") + expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to calendar") end end end @@ -187,35 +186,35 @@ RSpec.describe 'Calendar sharing via ical', js: true do # expect active sharing menu item within "#settingsDropdown" do - expect(page).to have_selector(".menu-item", text: "Subscribe to iCalendar") + expect(page).to have_selector(".menu-item", text: "Subscribe to calendar") end end it 'shows a sharing modal' do open_sharing_modal - expect(page).to have_selector('.spot-modal--header', text: "Subscribe to iCalendar") + expect(page).to have_selector('.spot-modal--header', text: "Subscribe to calendar") end it 'closes the sharing modal when closed by user by clicking the close button' do open_sharing_modal - expect(page).to have_selector('.spot-modal--header', text: "Subscribe to iCalendar") + expect(page).to have_selector('.spot-modal--header', text: "Subscribe to calendar") click_button "Cancel" - expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to iCalendar") + expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to calendar") end it 'successfully requests a new tokenized iCalendar URL when a unique name is provided' do open_sharing_modal - fill_in "Token name", with: "A token name" + fill_in "Where will you be using this?", with: "A token name" click_button "Copy URL" # implicitly testing for success -> modal is closed and fallback message is shown - expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to iCalendar") + expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to calendar") expect(page).to have_content("/projects/#{saved_query.project.id}/calendars/#{saved_query.id}/ical?ical_token=") # explictly testing for success message is not working in test env, probably @@ -236,30 +235,30 @@ RSpec.describe 'Calendar sharing via ical', js: true do click_button "Copy URL" # modal is still shown and error message is shown - expect(page).to have_selector('.spot-modal--header', text: "Subscribe to iCalendar") + expect(page).to have_selector('.spot-modal--header', text: "Subscribe to calendar") expect(page).to have_content("Name is mandatory") end it 'validates the uniqueness of a name' do open_sharing_modal - fill_in "Token name", with: "A token name" + fill_in "Where will you be using this?", with: "A token name" click_button "Copy URL" - expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to iCalendar") + expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to calendar") expect(page).to have_content("/projects/#{saved_query.project.id}/calendars/#{saved_query.id}/ical?ical_token=") # do the same thing again, now expect validation error open_sharing_modal - fill_in "Token name", with: "A token name" # same name for same user and same query -> not allowed + fill_in "Where will you be using this?", with: "A token name" # same name for same user and same query -> not allowed click_button "Copy URL" # modal is still shown and error message is shown - expect(page).to have_selector('.spot-modal--header', text: "Subscribe to iCalendar") + expect(page).to have_selector('.spot-modal--header', text: "Subscribe to calendar") expect(page).to have_content("Name is already in use") end end @@ -275,7 +274,7 @@ RSpec.describe 'Calendar sharing via ical', js: true do end expect(page).to have_selector(".title-container", text: "Working days") - click_link 'iCalendar' + click_link I18n.t(:label_calendar_subscriptions) expect(page) .to have_field('Enable iCalendar subscriptions', checked: true) @@ -309,12 +308,12 @@ RSpec.describe 'Calendar sharing via ical', js: true do # expect disabled sharing menu item within "#settingsDropdown" do - # expect(page).to have_button("Subscribe to iCalendar", disabled: true) # disabled selector not working - expect(page).to have_selector(".menu-item.inactive", text: "Subscribe to iCalendar") - page.click_button("Subscribe to iCalendar") + # expect(page).to have_button("Subscribe to calendar", disabled: true) # disabled selector not working + expect(page).to have_selector(".menu-item.inactive", text: "Subscribe to calendar") + page.click_button("Subscribe to calendar") # modal should not be shown - expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to iCalendar") + expect(page).not_to have_selector('.spot-modal--header', text: "Subscribe to calendar") end end end @@ -330,8 +329,8 @@ RSpec.describe 'Calendar sharing via ical', js: true do # expect disabled sharing menu item within "#settingsDropdown" do - expect(page).to have_selector(".menu-item", text: "Subscribe to iCalendar") - page.click_button("Subscribe to iCalendar") + expect(page).to have_selector(".menu-item", text: "Subscribe to calendar") + page.click_button("Subscribe to calendar") end end end diff --git a/modules/calendar/spec/services/create_ical_service_spec.rb b/modules/calendar/spec/services/create_ical_service_spec.rb index 0ecc957677b..c8f3e714ce1 100644 --- a/modules/calendar/spec/services/create_ical_service_spec.rb +++ b/modules/calendar/spec/services/create_ical_service_spec.rb @@ -61,14 +61,14 @@ RSpec.describe Calendar::CreateICalService, type: :model do described_class.new end - let(:freezed_date_time) { DateTime.now } + let(:frozen_date_time) { DateTime.now } let(:formatted_result) do subject.result.gsub("\r\n ", "").gsub("\r", "").gsub("\\n", "\n") end before do - Timecop.freeze(freezed_date_time) + Timecop.freeze(frozen_date_time) end subject do @@ -95,7 +95,7 @@ RSpec.describe Calendar::CreateICalService, type: :model do UID:#{work_package_with_due_date.id}@localhost:3000 DTSTART;VALUE=DATE:#{work_package_with_due_date.due_date.strftime('%Y%m%d')} DTEND;VALUE=DATE:#{(work_package_with_due_date.due_date + 1.day).strftime('%Y%m%d')} - DESCRIPTION:Project: #{project.name}\nType: None\nStatus: #{work_package_with_due_date.status.name}\nAssignee: \nPriority: #{work_package_with_due_date.priority.name}\n\nDescription:\n #{work_package_with_due_date.description} + DESCRIPTION:Project: #{project.name}\nType: None\nStatus: #{work_package_with_due_date.status.name}\nAssignee: \nPriority: #{work_package_with_due_date.priority.name}\n\nDescription:\n#{work_package_with_due_date.description} LOCATION:http://localhost:3000/work_packages/#{work_package_with_due_date.id} ORGANIZER;CN=#{work_package_with_due_date.author.name}:mailto:#{work_package_with_due_date.author.mail} SUMMARY:#{work_package_with_due_date.name} @@ -105,7 +105,7 @@ RSpec.describe Calendar::CreateICalService, type: :model do UID:#{work_package_with_start_date.id}@localhost:3000 DTSTART;VALUE=DATE:#{work_package_with_start_date.start_date.strftime('%Y%m%d')} DTEND;VALUE=DATE:#{(work_package_with_start_date.start_date + 1.day).strftime('%Y%m%d')} - DESCRIPTION:Project: #{project.name}\nType: None\nStatus: #{work_package_with_start_date.status.name}\nAssignee: \nPriority: #{work_package_with_start_date.priority.name}\n\nDescription:\n #{work_package_with_start_date.description} + DESCRIPTION:Project: #{project.name}\nType: None\nStatus: #{work_package_with_start_date.status.name}\nAssignee: \nPriority: #{work_package_with_start_date.priority.name}\n\nDescription:\n#{work_package_with_start_date.description} LOCATION:http://localhost:3000/work_packages/#{work_package_with_start_date.id} ORGANIZER;CN=#{work_package_with_start_date.author.name}:mailto:#{work_package_with_start_date.author.mail} SUMMARY:#{work_package_with_start_date.name} @@ -115,7 +115,7 @@ RSpec.describe Calendar::CreateICalService, type: :model do UID:#{work_package_with_start_and_due_date.id}@localhost:3000 DTSTART;VALUE=DATE:#{work_package_with_start_and_due_date.start_date.strftime('%Y%m%d')} DTEND;VALUE=DATE:#{(work_package_with_start_and_due_date.due_date + 1.day).strftime('%Y%m%d')} - DESCRIPTION:Project: #{project.name}\nType: None\nStatus: #{work_package_with_start_and_due_date.status.name}\nAssignee: \nPriority: #{work_package_with_start_and_due_date.priority.name}\n\nDescription:\n #{work_package_with_start_and_due_date.description} + DESCRIPTION:Project: #{project.name}\nType: None\nStatus: #{work_package_with_start_and_due_date.status.name}\nAssignee: \nPriority: #{work_package_with_start_and_due_date.priority.name}\n\nDescription:\n#{work_package_with_start_and_due_date.description} LOCATION:http://localhost:3000/work_packages/#{work_package_with_start_and_due_date.id} ORGANIZER;CN=#{work_package_with_start_and_due_date.author.name}:mailto:#{work_package_with_start_and_due_date.author.mail} SUMMARY:#{work_package_with_start_and_due_date.name} @@ -125,7 +125,7 @@ RSpec.describe Calendar::CreateICalService, type: :model do UID:#{work_package_with_due_date_and_assignee.id}@localhost:3000 DTSTART;VALUE=DATE:#{work_package_with_due_date_and_assignee.due_date.strftime('%Y%m%d')} DTEND;VALUE=DATE:#{(work_package_with_due_date_and_assignee.due_date + 1.day).strftime('%Y%m%d')} - DESCRIPTION:Project: #{project.name}\nType: None\nStatus: #{work_package_with_due_date_and_assignee.status.name}\nAssignee: #{work_package_with_due_date_and_assignee.assigned_to.name}\nPriority: #{work_package_with_due_date_and_assignee.priority.name}\n\nDescription:\n #{work_package_with_due_date_and_assignee.description} + DESCRIPTION:Project: #{project.name}\nType: None\nStatus: #{work_package_with_due_date_and_assignee.status.name}\nAssignee: #{work_package_with_due_date_and_assignee.assigned_to.name}\nPriority: #{work_package_with_due_date_and_assignee.priority.name}\n\nDescription:\n#{work_package_with_due_date_and_assignee.description} LOCATION:http://localhost:3000/work_packages/#{work_package_with_due_date_and_assignee.id} ORGANIZER;CN=#{work_package_with_due_date_and_assignee.author.name}:mailto:#{work_package_with_due_date_and_assignee.author.mail} SUMMARY:#{work_package_with_due_date_and_assignee.name} diff --git a/modules/costs/config/locales/crowdin/ne.yml b/modules/costs/config/locales/crowdin/ne.yml index a592f7cea2a..82a25daaf10 100644 --- a/modules/costs/config/locales/crowdin/ne.yml +++ b/modules/costs/config/locales/crowdin/ne.yml @@ -80,7 +80,7 @@ ne: label_costlog: "Logged unit costs" label_cost_plural: "Costs" label_cost_type_plural: "Cost types" - label_cost_type_specific: "Cost type #%{id}: %{name}" #%{id}: %{name}" + label_cost_type_specific: "Cost type #%{id}: %{name}" label_costs_per_page: "Costs per page" label_currency: "Currency" label_currency_format: "Format of currency" diff --git a/modules/documents/lib/open_project/documents/engine.rb b/modules/documents/lib/open_project/documents/engine.rb index 2c68809a421..05fb8956ffd 100644 --- a/modules/documents/lib/open_project/documents/engine.rb +++ b/modules/documents/lib/open_project/documents/engine.rb @@ -74,7 +74,5 @@ module OpenProject::Documents # Add documents to allowed search params additional_permitted_attributes search: %i(documents) - - patch_with_namespace :OpenProject, :TextFormatting, :Formats, :Markdown, :TextileConverter end end diff --git a/modules/documents/lib/open_project/documents/patches/textile_converter_patch.rb b/modules/documents/lib/open_project/documents/patches/textile_converter_patch.rb deleted file mode 100644 index 97bf04aa866..00000000000 --- a/modules/documents/lib/open_project/documents/patches/textile_converter_patch.rb +++ /dev/null @@ -1,42 +0,0 @@ -#-- copyright -# OpenProject Documents Plugin -# -# Former OpenProject Core functionality extracted into a plugin. -# -# Copyright (C) 2009-2014 the OpenProject Foundation (OPF) -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#+ - -module OpenProject::Documents::Patches - module TextileConverterPatch - def models_to_convert - super.merge(::Document => [:description]) - end - end -end - -OpenProject::TextFormatting::Formats::Markdown::TextileConverter.prepend( - OpenProject::Documents::Patches::TextileConverterPatch -) diff --git a/modules/github_integration/config/locales/js-en.yml b/modules/github_integration/config/locales/js-en.yml index 47236883571..247437c2e54 100644 --- a/modules/github_integration/config/locales/js-en.yml +++ b/modules/github_integration/config/locales/js-en.yml @@ -48,8 +48,8 @@ en: github_actions: Actions pull_requests: - message: "Pull request #%{pr_number} %{pr_link} for %{repository_link} has been %{pr_state} by %{github_user_link}." - referenced_message: "%{github_user_link} referenced this work package in pull request #%{pr_number} %{pr_link} on %{repository_link}." + message: "Pull request #%{pr_number} %{pr_link} for %{repository_link} authored by %{github_user_link} has been %{pr_state}." + referenced_message: "Pull request #%{pr_number} %{pr_link} for %{repository_link} authored by %{github_user_link} referenced this work package." states: opened: 'opened' closed: 'closed' diff --git a/modules/github_integration/frontend/module/pull-request/pull-request-macro.component.ts b/modules/github_integration/frontend/module/pull-request/pull-request-macro.component.ts index 712d83588fd..72c995616a3 100644 --- a/modules/github_integration/frontend/module/pull-request/pull-request-macro.component.ts +++ b/modules/github_integration/frontend/module/pull-request/pull-request-macro.component.ts @@ -36,7 +36,7 @@ import { } from '@angular/core'; import { HalResourceEditingService } from 'core-app/shared/components/fields/edit/services/hal-resource-editing.service'; import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs'; -import { IGithubPullRequest } from '../state/github-pull-request.model'; +import { IGithubPullRequest, IGithubUserResource } from '../state/github-pull-request.model'; import { GithubPullRequestResourceService } from '../state/github-pull-request.service'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -85,7 +85,8 @@ export class PullRequestMacroComponent implements OnInit { } private buildText(pr:IGithubPullRequest):string { - const githubUserLink = this.htmlLink(pr._embedded.githubUser.htmlUrl, pr._embedded.githubUser.login); + const actor = this.deriveActor(pr) as IGithubUserResource; + const actorLink = this.htmlLink(actor.htmlUrl, actor.login); const repositoryLink = this.htmlLink(pr.repositoryHtmlUrl, pr.repository); const prLink = this.htmlLink(pr.htmlUrl, pr.title); @@ -100,11 +101,20 @@ export class PullRequestMacroComponent implements OnInit { `js.github_integration.pull_requests.states.${this.pullRequestState}`, { defaultValue: this.pullRequestState || '(unknown state)' }, ), - github_user_link: githubUserLink, + github_user_link: actorLink, }, ); } + + private deriveActor(pr:IGithubPullRequest) { + if (this.pullRequestState === 'merged') { + return pr._embedded.mergedBy; + } else { + return pr._embedded.githubUser; + } + } + private htmlLink(href:string, title:string):string { const link = document.createElement('a'); link.href = href; diff --git a/modules/github_integration/spec/features/work_package_activity_spec.rb b/modules/github_integration/spec/features/work_package_activity_spec.rb new file mode 100644 index 00000000000..27b3c98eeff --- /dev/null +++ b/modules/github_integration/spec/features/work_package_activity_spec.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Work Package Activity Tab', + 'Comments by Github', + :js, + :with_cuprite do + shared_let(:github_system_user) { create(:admin, firstname: 'Github', lastname: 'System User') } + shared_let(:admin) { create(:admin) } + + shared_let(:project) { create(:project, enabled_module_names: Setting.default_projects_modules + %w[activity]) } + shared_let(:work_package) { create(:work_package, project:) } + + shared_let(:pull_request_author) { create(:github_user, github_login: 'i_am_the_author') } + shared_let(:pull_request_merging_user) { create(:github_user, github_login: 'i_merged') } + shared_let(:pull_request) do + create(:github_pull_request, + github_user: pull_request_author) + end + + def trigger_pull_request_action + OpenProject::GithubIntegration::NotificationHandler::PullRequest.new + .process(payload) + + pull_request.reload + end + + let(:payload) do + { + 'action' => action, + 'open_project_user_id' => github_system_user.id, + 'pull_request' => pull_request_hash, + 'sender' => sender_section_hash + } + end + + let(:pull_request_hash) do + { + 'id' => pull_request.id, + 'number' => pull_request.number, + 'body' => "Mentioning OP##{work_package.id}", + 'title' => pull_request.title, + 'html_url' => pull_request.github_html_url, + 'updated_at' => Time.current.iso8601, + 'state' => state, + 'draft' => false, + 'merged' => merged, + 'merged_by' => merged_by_section_hash, + 'merged_at' => merged_at, + 'comments' => pull_request.comments_count + 1, + 'review_comments' => pull_request.review_comments_count, + 'additions' => pull_request.additions_count, + 'deletions' => pull_request.deletions_count, + 'changed_files' => pull_request.changed_files_count, + 'labels' => pull_request.labels, + 'user' => { + 'id' => pull_request_author.github_id, + 'login' => pull_request_author.github_login, + 'html_url' => pull_request_author.github_html_url, + 'avatar_url' => pull_request_author.github_avatar_url + }, + 'base' => { + 'repo' => { + 'full_name' => 'author_user/repo', + 'html_url' => 'github.com/author_user/repo' + } + } + } + end + + let(:sender_section_hash) do + { + 'login' => 'test_user', + 'html_url' => 'github.com/test_user' + } + end + + let(:work_package_page) { Pages::SplitWorkPackage.new(work_package, project) } + + context 'when the pull request is merged' do + let(:action) { 'closed' } + + before do + trigger_pull_request_action + login_as admin + end + + context "and I visit the work package's activity tab" do + let(:merged) { true } + let(:merged_by_section_hash) do + { + 'id' => pull_request_merging_user.github_id, + 'login' => pull_request_merging_user.github_login, + 'html_url' => pull_request_merging_user.github_html_url, + 'avatar_url' => pull_request_merging_user.github_avatar_url + } + end + let(:merged_at) { Time.current.iso8601 } + let(:state) { 'closed' } + + before do + work_package_page.visit_tab! 'activity' + work_package_page.ensure_page_loaded + end + + it 'renders a comment stating the Pull Request was merged by the merge actor' do + expected_merge_comment = <<~GITHUB_MERGE_COMMENT.squish + Merged#{I18n.t('js.github_integration.pull_requests.message', + pr_number: pull_request.number, + pr_link: pull_request.title, + repository_link: pull_request.repository, + pr_state: 'merged', + github_user_link: pull_request_merging_user.github_login)} + GITHUB_MERGE_COMMENT + + expect(page).to have_selector('.user-comment > .message', text: expected_merge_comment) + end + end + end + + context 'when the Work Package is referenced in a Pull Request' do + let(:action) { 'referenced' } + + let(:merged) { false } + let(:merged_by_section_hash) { {} } + let(:merged_at) { nil } + let(:state) { 'open' } + + before do + trigger_pull_request_action + login_as admin + end + + context "and I visit the work package's activity tab" do + before do + work_package_page.visit_tab! 'activity' + work_package_page.ensure_page_loaded + end + + it 'renders a comment stating the Work Package was referenced in the Pull Request' do + expected_referenced_comment = <<~GITHUB_REFERENCED_COMMENT.squish + Referenced#{I18n.t('js.github_integration.pull_requests.referenced_message', + pr_number: pull_request.number, + pr_link: pull_request.title, + repository_link: pull_request.repository, + pr_state: 'referenced', + github_user_link: pull_request_author.github_login)} + GITHUB_REFERENCED_COMMENT + + expect(page).to have_selector('.user-comment > .message', text: expected_referenced_comment) + end + end + end + + context 'when any non-edge-case action is performed on a pull request' do + let(:action) { 'ready_for_review' } + + let(:merged) { false } + let(:merged_by_section_hash) { {} } + let(:merged_at) { nil } + let(:state) { 'open' } + + before do + trigger_pull_request_action + login_as admin + end + + context "and I visit the work package's activity tab" do + before do + work_package_page.visit_tab! 'activity' + work_package_page.ensure_page_loaded + end + + it 'renders a comment stating that said action was performed on the Pull Request' do + expected_action_comment = <<~GITHUB_READY_FOR_REVIEW_COMMENT.squish + Marked Ready For Review#{I18n.t('js.github_integration.pull_requests.message', + pr_number: pull_request.number, + pr_link: pull_request.title, + repository_link: pull_request.repository, + pr_state: 'marked ready for review', + github_user_link: pull_request_author.github_login)} + GITHUB_READY_FOR_REVIEW_COMMENT + + expect(page).to have_selector('.user-comment > .message', text: expected_action_comment) + end + end + end +end diff --git a/modules/ldap_groups/app/components/ldap_groups/synchronized_filters/row_component.rb b/modules/ldap_groups/app/components/ldap_groups/synchronized_filters/row_component.rb index 5e1cde0fb7d..9d2346a543f 100644 --- a/modules/ldap_groups/app/components/ldap_groups/synchronized_filters/row_component.rb +++ b/modules/ldap_groups/app/components/ldap_groups/synchronized_filters/row_component.rb @@ -57,10 +57,12 @@ module LdapGroups [ edit_link, delete_link - ] + ].compact end def edit_link + return if model.seeded_from_env? + link_to I18n.t(:button_edit), { controller: table.target_controller, ldap_filter_id: model.id, action: :edit }, class: 'icon icon-edit', @@ -68,6 +70,8 @@ module LdapGroups end def delete_link + return if model.seeded_from_env? + link_to I18n.t(:button_delete), { controller: table.target_controller, ldap_filter_id: model.id, action: :destroy_info }, class: 'icon icon-delete', diff --git a/modules/ldap_groups/app/models/ldap_groups/synchronized_filter.rb b/modules/ldap_groups/app/models/ldap_groups/synchronized_filter.rb index b0e5564be45..43baa15f7ce 100644 --- a/modules/ldap_groups/app/models/ldap_groups/synchronized_filter.rb +++ b/modules/ldap_groups/app/models/ldap_groups/synchronized_filter.rb @@ -10,6 +10,7 @@ module LdapGroups foreign_key: 'filter_id', dependent: :destroy + validates_presence_of :name validates_presence_of :filter_string validates_presence_of :ldap_auth_source validate :validate_filter_syntax @@ -23,6 +24,13 @@ module LdapGroups base_dn.presence || ldap_auth_source.base_dn end + def seeded_from_env? + return false if ldap_auth_source.nil? + + ldap_auth_source&.seeded_from_env? && + Setting.seed_ldap.dig(ldap_auth_source.name, 'groupfilter', name) + end + private def validate_filter_syntax diff --git a/modules/ldap_groups/app/views/ldap_groups/synchronized_filters/show.html.erb b/modules/ldap_groups/app/views/ldap_groups/synchronized_filters/show.html.erb index 21f6f7c89b5..e43b446b714 100644 --- a/modules/ldap_groups/app/views/ldap_groups/synchronized_filters/show.html.erb +++ b/modules/ldap_groups/app/views/ldap_groups/synchronized_filters/show.html.erb @@ -1,24 +1,36 @@ <% html_title(t(:label_administration), t('ldap_groups.synchronized_filters.plural'), h(@filter.name)) -%> <%= error_messages_for @filter %> +<% blocked = @filter.seeded_from_env? %> <%= breadcrumb_toolbar(h(@filter.name)) do %> -
  • - <%= link_to({ action: :edit }, - class: 'button') do %> - <%= op_icon('button--icon icon-edit') %> - <%= t(:button_edit) %> - <% end %> -
  • -
  • - <%= link_to({ action: :destroy_info }, - class: 'button -danger') do %> - <%= op_icon('button--icon icon-delete') %> - <%= t(:button_delete) %> - <% end %> -
  • + <% unless blocked %> +
  • + <%= link_to({ action: :edit }, + class: 'button') do %> + <%= op_icon('button--icon icon-edit') %> + <%= t(:button_edit) %> + <% end %> +
  • +
  • + <%= link_to({ action: :destroy_info }, + class: 'button -danger') do %> + <%= op_icon('button--icon icon-delete') %> + <%= t(:button_delete) %> + <% end %> +
  • + <% end %> <% end %> +<% if blocked %> +
    +
    + <%= t(:label_seeded_from_env_warning) %> +
    +
    +<% end %> + +
    <%= render(AttributeGroups::AttributeGroupComponent.new) do |component| component.with_attribute(key: @filter.class.human_attribute_name(:name), @@ -29,10 +41,10 @@ value: @filter.used_base_dn) component.with_attribute(key: @filter.class.human_attribute_name(:sync_users), value: if @filter.sync_users - checked_image @filter.sync_users - else - t(:general_text_no) - end) + checked_image @filter.sync_users + else + t(:general_text_no) + end) component.with_attribute(key: @filter.class.human_attribute_name(:filter_string), value: @filter.filter_string) component.with_attribute(key: t('ldap_groups.synchronized_groups.plural'), @@ -41,14 +53,14 @@
    -<%= toolbar(title: t('ldap_groups.synchronized_groups.plural')) do %> -
  • - <%= link_to({ action: :synchronize }, - class: 'button') do %> - <%= t('ldap_groups.label_synchronize') %> - <% end %> -
  • -<% end %> + <%= toolbar(title: t('ldap_groups.synchronized_groups.plural')) do %> +
  • + <%= link_to({ action: :synchronize }, + class: 'button') do %> + <%= t('ldap_groups.label_synchronize') %> + <% end %> +
  • + <% end %> <%= render ::LdapGroups::SynchronizedGroups::TableComponent.new(rows: @filter.groups, show_inline_create: false, deletable: false) %>
    diff --git a/modules/ldap_groups/config/locales/crowdin/af.yml b/modules/ldap_groups/config/locales/crowdin/af.yml index 716d99c1f26..e9a789b3674 100644 --- a/modules/ldap_groups/config/locales/crowdin/af.yml +++ b/modules/ldap_groups/config/locales/crowdin/af.yml @@ -3,11 +3,11 @@ af: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/ar.yml b/modules/ldap_groups/config/locales/crowdin/ar.yml index 012c5c3ba03..84e90860ba2 100644 --- a/modules/ldap_groups/config/locales/crowdin/ar.yml +++ b/modules/ldap_groups/config/locales/crowdin/ar.yml @@ -3,11 +3,11 @@ ar: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'اتصال LDAP' + ldap_auth_source: 'LDAP connection' sync_users: 'مزامنة المستخدمين' ldap_groups/synchronized_filter: filter_string: 'مرشّح LDAP' - auth_source: 'اتصال LDAP' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'مزامنة المستخدمين' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/az.yml b/modules/ldap_groups/config/locales/crowdin/az.yml index ebbd28b71a1..7dba2d0e033 100644 --- a/modules/ldap_groups/config/locales/crowdin/az.yml +++ b/modules/ldap_groups/config/locales/crowdin/az.yml @@ -3,11 +3,11 @@ az: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/be.yml b/modules/ldap_groups/config/locales/crowdin/be.yml index 0f769cc30be..907b53a7acf 100644 --- a/modules/ldap_groups/config/locales/crowdin/be.yml +++ b/modules/ldap_groups/config/locales/crowdin/be.yml @@ -3,11 +3,11 @@ be: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/bg.yml b/modules/ldap_groups/config/locales/crowdin/bg.yml index bb57fb040b4..182fd68d4f5 100644 --- a/modules/ldap_groups/config/locales/crowdin/bg.yml +++ b/modules/ldap_groups/config/locales/crowdin/bg.yml @@ -3,11 +3,11 @@ bg: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/ca.yml b/modules/ldap_groups/config/locales/crowdin/ca.yml index 31d78fe6fe2..a32ccf1d9cc 100644 --- a/modules/ldap_groups/config/locales/crowdin/ca.yml +++ b/modules/ldap_groups/config/locales/crowdin/ca.yml @@ -3,11 +3,11 @@ ca: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'Connexió LDAP' + ldap_auth_source: 'LDAP connection' sync_users: 'Usuaris sincronitzats' ldap_groups/synchronized_filter: filter_string: 'Filtre LDAP' - auth_source: 'Connexió LDAP' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Atribut de nom de grup" sync_users: 'Usuaris sincronitzats' base_dn: "Base de cerca DN" diff --git a/modules/ldap_groups/config/locales/crowdin/ckb-IR.yml b/modules/ldap_groups/config/locales/crowdin/ckb-IR.yml index 86da64a7c21..2338e8fa9df 100644 --- a/modules/ldap_groups/config/locales/crowdin/ckb-IR.yml +++ b/modules/ldap_groups/config/locales/crowdin/ckb-IR.yml @@ -3,11 +3,11 @@ ckb-IR: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/cs.yml b/modules/ldap_groups/config/locales/crowdin/cs.yml index e01041239e3..7708cbfc38c 100644 --- a/modules/ldap_groups/config/locales/crowdin/cs.yml +++ b/modules/ldap_groups/config/locales/crowdin/cs.yml @@ -3,11 +3,11 @@ cs: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'Připojení LDAP' + ldap_auth_source: 'Připojení LDAP' sync_users: 'Synchronizovat uživatele' ldap_groups/synchronized_filter: filter_string: 'LDAP filtr' - auth_source: 'Připojení LDAP' + ldap_auth_source: 'Připojení LDAP' group_name_attribute: "Atribut názvu skupiny" sync_users: 'Synchronizovat uživatele' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/da.yml b/modules/ldap_groups/config/locales/crowdin/da.yml index 787900a906e..4de06cd6148 100644 --- a/modules/ldap_groups/config/locales/crowdin/da.yml +++ b/modules/ldap_groups/config/locales/crowdin/da.yml @@ -3,11 +3,11 @@ da: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Synkroniser brugere' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Synkroniser brugere' base_dn: "Søg i base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/de.yml b/modules/ldap_groups/config/locales/crowdin/de.yml index 0dac9d97352..c6222173bc1 100644 --- a/modules/ldap_groups/config/locales/crowdin/de.yml +++ b/modules/ldap_groups/config/locales/crowdin/de.yml @@ -3,11 +3,11 @@ de: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP-Verbindung' + ldap_auth_source: 'LDAP connection' sync_users: 'Benutzer synchronisieren' ldap_groups/synchronized_filter: filter_string: 'LDAP-Filter' - auth_source: 'LDAP-Verbindung' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Attribut für Gruppenname" sync_users: 'Benutzer synchronisieren' base_dn: "Suchbasis DN" diff --git a/modules/ldap_groups/config/locales/crowdin/el.yml b/modules/ldap_groups/config/locales/crowdin/el.yml index b2793a4649d..2c098a81bf8 100644 --- a/modules/ldap_groups/config/locales/crowdin/el.yml +++ b/modules/ldap_groups/config/locales/crowdin/el.yml @@ -3,11 +3,11 @@ el: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'Σύνδεση LDAP' + ldap_auth_source: 'LDAP connection' sync_users: 'Συγχρονισμός χρηστών' ldap_groups/synchronized_filter: filter_string: 'LDAP Φίλτρο' - auth_source: 'Σύνδεση LDAP' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Χαρακτηριστικό ονόματος ομάδας" sync_users: 'Συγχρονισμός χρηστών' base_dn: "Αναζήτηση base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/eo.yml b/modules/ldap_groups/config/locales/crowdin/eo.yml index c41eda25a0c..16a6f5528d2 100644 --- a/modules/ldap_groups/config/locales/crowdin/eo.yml +++ b/modules/ldap_groups/config/locales/crowdin/eo.yml @@ -3,11 +3,11 @@ eo: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/es.yml b/modules/ldap_groups/config/locales/crowdin/es.yml index 5c2b1f6b2a3..fcf0bb892b3 100644 --- a/modules/ldap_groups/config/locales/crowdin/es.yml +++ b/modules/ldap_groups/config/locales/crowdin/es.yml @@ -3,11 +3,11 @@ es: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'Conexión LDAP' + ldap_auth_source: 'LDAP connection' sync_users: 'Sincronizar usuarios' ldap_groups/synchronized_filter: filter_string: 'Filtro LDAP' - auth_source: 'Conexión LDAP' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Atributo de nombre de grupo" sync_users: 'Sincronizar usuarios' base_dn: "DN base de búsqueda" diff --git a/modules/ldap_groups/config/locales/crowdin/et.yml b/modules/ldap_groups/config/locales/crowdin/et.yml index a4bff7dc568..33a33ce9ef0 100644 --- a/modules/ldap_groups/config/locales/crowdin/et.yml +++ b/modules/ldap_groups/config/locales/crowdin/et.yml @@ -3,11 +3,11 @@ et: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/eu.yml b/modules/ldap_groups/config/locales/crowdin/eu.yml index bcf7eb41500..c918ae55f1b 100644 --- a/modules/ldap_groups/config/locales/crowdin/eu.yml +++ b/modules/ldap_groups/config/locales/crowdin/eu.yml @@ -3,11 +3,11 @@ eu: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/fa.yml b/modules/ldap_groups/config/locales/crowdin/fa.yml index f1b2231668c..6fc374fe231 100644 --- a/modules/ldap_groups/config/locales/crowdin/fa.yml +++ b/modules/ldap_groups/config/locales/crowdin/fa.yml @@ -3,11 +3,11 @@ fa: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'همسان سازی کاربران' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'همسان سازی کاربران' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/fi.yml b/modules/ldap_groups/config/locales/crowdin/fi.yml index dc4fe1c07f7..11179a4e0fa 100644 --- a/modules/ldap_groups/config/locales/crowdin/fi.yml +++ b/modules/ldap_groups/config/locales/crowdin/fi.yml @@ -3,11 +3,11 @@ fi: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/fil.yml b/modules/ldap_groups/config/locales/crowdin/fil.yml index 67d6d6e0818..d42f2162b71 100644 --- a/modules/ldap_groups/config/locales/crowdin/fil.yml +++ b/modules/ldap_groups/config/locales/crowdin/fil.yml @@ -3,11 +3,11 @@ fil: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/fr.yml b/modules/ldap_groups/config/locales/crowdin/fr.yml index 377375d67ce..df62488c4c8 100644 --- a/modules/ldap_groups/config/locales/crowdin/fr.yml +++ b/modules/ldap_groups/config/locales/crowdin/fr.yml @@ -3,11 +3,11 @@ fr: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'Connexion LDAP' + ldap_auth_source: 'LDAP connection' sync_users: 'Synchroniser les utilisateurs' ldap_groups/synchronized_filter: filter_string: 'Filtre LDAP' - auth_source: 'Connexion LDAP' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Attribut nom de groupe" sync_users: 'Synchroniser les utilisateurs' base_dn: "Rechercher dans la base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/he.yml b/modules/ldap_groups/config/locales/crowdin/he.yml index 5f6887d6b34..99982f1c621 100644 --- a/modules/ldap_groups/config/locales/crowdin/he.yml +++ b/modules/ldap_groups/config/locales/crowdin/he.yml @@ -3,11 +3,11 @@ he: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/hi.yml b/modules/ldap_groups/config/locales/crowdin/hi.yml index ddc8a266240..657d4888a3c 100644 --- a/modules/ldap_groups/config/locales/crowdin/hi.yml +++ b/modules/ldap_groups/config/locales/crowdin/hi.yml @@ -3,11 +3,11 @@ hi: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/hr.yml b/modules/ldap_groups/config/locales/crowdin/hr.yml index e06f78849b0..e98de2e6db6 100644 --- a/modules/ldap_groups/config/locales/crowdin/hr.yml +++ b/modules/ldap_groups/config/locales/crowdin/hr.yml @@ -3,11 +3,11 @@ hr: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/hu.yml b/modules/ldap_groups/config/locales/crowdin/hu.yml index aea207c6c9f..dc61517e630 100644 --- a/modules/ldap_groups/config/locales/crowdin/hu.yml +++ b/modules/ldap_groups/config/locales/crowdin/hu.yml @@ -3,11 +3,11 @@ hu: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP csatlakozás' + ldap_auth_source: 'LDAP connection' sync_users: 'Felhasználók szinkronizálása' ldap_groups/synchronized_filter: filter_string: 'LDAP szűrő' - auth_source: 'LDAP csatlakozás' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Csoport név attribútum" sync_users: 'Felhasználók szinkronizálása' base_dn: "Keresési alap DN" diff --git a/modules/ldap_groups/config/locales/crowdin/id.yml b/modules/ldap_groups/config/locales/crowdin/id.yml index d6f817e0c36..732901f0e2d 100644 --- a/modules/ldap_groups/config/locales/crowdin/id.yml +++ b/modules/ldap_groups/config/locales/crowdin/id.yml @@ -3,11 +3,11 @@ id: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'koneksi LDAP' + ldap_auth_source: 'LDAP connection' sync_users: 'Sinkronkan pengguna' ldap_groups/synchronized_filter: filter_string: 'penyaring LDAP' - auth_source: 'koneksi LDAP' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Atribut nama grup" sync_users: 'Sinkronkan pengguna' base_dn: "Cari basis DN" diff --git a/modules/ldap_groups/config/locales/crowdin/it.yml b/modules/ldap_groups/config/locales/crowdin/it.yml index bf1c2bb7d60..62dccdcd168 100644 --- a/modules/ldap_groups/config/locales/crowdin/it.yml +++ b/modules/ldap_groups/config/locales/crowdin/it.yml @@ -3,11 +3,11 @@ it: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'Connessione LDAP' + ldap_auth_source: 'LDAP connection' sync_users: 'Sincronizza utenti' ldap_groups/synchronized_filter: filter_string: 'Filtro LDAP' - auth_source: 'Connessione LDAP' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Attributo nome gruppo" sync_users: 'Sincronizza utenti' base_dn: "Ricerca DN base" diff --git a/modules/ldap_groups/config/locales/crowdin/ja.yml b/modules/ldap_groups/config/locales/crowdin/ja.yml index 6e87f689c31..5423a7de934 100644 --- a/modules/ldap_groups/config/locales/crowdin/ja.yml +++ b/modules/ldap_groups/config/locales/crowdin/ja.yml @@ -3,11 +3,11 @@ ja: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP 接続' + ldap_auth_source: 'LDAP connection' sync_users: 'ユーザーを同期' ldap_groups/synchronized_filter: filter_string: 'LDAPフィルタ' - auth_source: 'LDAP 接続' + ldap_auth_source: 'LDAP connection' group_name_attribute: "グループ名属性" sync_users: 'ユーザーを同期' base_dn: "ベース DN を検索" diff --git a/modules/ldap_groups/config/locales/crowdin/ka.yml b/modules/ldap_groups/config/locales/crowdin/ka.yml index efab8240ffa..4bf7e238182 100644 --- a/modules/ldap_groups/config/locales/crowdin/ka.yml +++ b/modules/ldap_groups/config/locales/crowdin/ka.yml @@ -3,11 +3,11 @@ ka: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/ko.yml b/modules/ldap_groups/config/locales/crowdin/ko.yml index 4848bf49ee0..4691b07ca81 100644 --- a/modules/ldap_groups/config/locales/crowdin/ko.yml +++ b/modules/ldap_groups/config/locales/crowdin/ko.yml @@ -3,11 +3,11 @@ ko: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP 연결' + ldap_auth_source: 'LDAP connection' sync_users: '사용자 동기화' ldap_groups/synchronized_filter: filter_string: 'LDAP 필터' - auth_source: 'LDAP 연결' + ldap_auth_source: 'LDAP connection' group_name_attribute: "그룹 이름 특성" sync_users: '사용자 동기화' base_dn: "기본 DN 검색" diff --git a/modules/ldap_groups/config/locales/crowdin/lt.yml b/modules/ldap_groups/config/locales/crowdin/lt.yml index 484fee5c556..c1b3776a1df 100644 --- a/modules/ldap_groups/config/locales/crowdin/lt.yml +++ b/modules/ldap_groups/config/locales/crowdin/lt.yml @@ -3,11 +3,11 @@ lt: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP jungtis' + ldap_auth_source: 'LDAP jungtis' sync_users: 'Sinchronizuoti naudotojus' ldap_groups/synchronized_filter: filter_string: 'LDAP filtras' - auth_source: 'LDAP jungtis' + ldap_auth_source: 'LDAP jungtis' group_name_attribute: "Grupės pavadinimo atributas" sync_users: 'Sinchronizuoti naudotojus' base_dn: "Paieškos pagrindo DN" diff --git a/modules/ldap_groups/config/locales/crowdin/lv.yml b/modules/ldap_groups/config/locales/crowdin/lv.yml index 1c707ffe16b..4382d67d73e 100644 --- a/modules/ldap_groups/config/locales/crowdin/lv.yml +++ b/modules/ldap_groups/config/locales/crowdin/lv.yml @@ -3,11 +3,11 @@ lv: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/mn.yml b/modules/ldap_groups/config/locales/crowdin/mn.yml index 1ec02387139..0ecea5f25a0 100644 --- a/modules/ldap_groups/config/locales/crowdin/mn.yml +++ b/modules/ldap_groups/config/locales/crowdin/mn.yml @@ -3,11 +3,11 @@ mn: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/ne.yml b/modules/ldap_groups/config/locales/crowdin/ne.yml index 85f7ad7e6d4..a4146a9d926 100644 --- a/modules/ldap_groups/config/locales/crowdin/ne.yml +++ b/modules/ldap_groups/config/locales/crowdin/ne.yml @@ -3,11 +3,11 @@ ne: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/nl.yml b/modules/ldap_groups/config/locales/crowdin/nl.yml index 6204607289b..aa2c8e4310b 100644 --- a/modules/ldap_groups/config/locales/crowdin/nl.yml +++ b/modules/ldap_groups/config/locales/crowdin/nl.yml @@ -3,11 +3,11 @@ nl: attributes: ldap_groups/synchronized_group: dn: 'AFW' - auth_source: 'LDAP verbinding' + ldap_auth_source: 'LDAP connection' sync_users: 'Gebruikers synchroniseren' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP verbinding' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Attribuut groepsnaam" sync_users: 'Gebruikers synchroniseren' base_dn: "Zoek basis DN" diff --git a/modules/ldap_groups/config/locales/crowdin/no.yml b/modules/ldap_groups/config/locales/crowdin/no.yml index 6376b3cf5d5..1da1bd4e68e 100644 --- a/modules/ldap_groups/config/locales/crowdin/no.yml +++ b/modules/ldap_groups/config/locales/crowdin/no.yml @@ -3,11 +3,11 @@ attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/pl.yml b/modules/ldap_groups/config/locales/crowdin/pl.yml index 7e2d965c96b..84717049796 100644 --- a/modules/ldap_groups/config/locales/crowdin/pl.yml +++ b/modules/ldap_groups/config/locales/crowdin/pl.yml @@ -3,11 +3,11 @@ pl: attributes: ldap_groups/synchronized_group: dn: 'Nazwa wyróżniająca' - auth_source: 'Połączenie LDAP' + ldap_auth_source: 'LDAP connection' sync_users: 'Synchronizuj użytkowników' ldap_groups/synchronized_filter: filter_string: 'Filtr LDAP' - auth_source: 'Połączenie LDAP' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Atrybut nazwy grupy" sync_users: 'Synchronizuj użytkowników' base_dn: "Wyszukaj bazową nazwę wyróżniającą" diff --git a/modules/ldap_groups/config/locales/crowdin/pt.yml b/modules/ldap_groups/config/locales/crowdin/pt.yml index 6d2b3d7bebe..9e32a30ad3e 100644 --- a/modules/ldap_groups/config/locales/crowdin/pt.yml +++ b/modules/ldap_groups/config/locales/crowdin/pt.yml @@ -3,11 +3,11 @@ pt: attributes: ldap_groups/synchronized_group: dn: 'ND' - auth_source: 'Conexão LDAP' + ldap_auth_source: 'LDAP connection' sync_users: 'Sincronizar usuários' ldap_groups/synchronized_filter: filter_string: 'Filtro LDAP' - auth_source: 'Conexão LDAP' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Atributo de nome de grupo" sync_users: 'Sincronizar usuários' base_dn: "Procurar DN base" diff --git a/modules/ldap_groups/config/locales/crowdin/ro.yml b/modules/ldap_groups/config/locales/crowdin/ro.yml index 36eec601971..b27191c5f04 100644 --- a/modules/ldap_groups/config/locales/crowdin/ro.yml +++ b/modules/ldap_groups/config/locales/crowdin/ro.yml @@ -3,11 +3,11 @@ ro: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'Conexiune LDAP' + ldap_auth_source: 'LDAP connection' sync_users: 'Sincronizați utilizatorii' ldap_groups/synchronized_filter: filter_string: 'Filtru LDAP' - auth_source: 'Conexiune LDAP' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Atributul nume de grup" sync_users: 'Sincronizați utilizatorii' base_dn: "Baza de căutare DN" diff --git a/modules/ldap_groups/config/locales/crowdin/ru.yml b/modules/ldap_groups/config/locales/crowdin/ru.yml index e6f6ce46b42..078f6f91934 100644 --- a/modules/ldap_groups/config/locales/crowdin/ru.yml +++ b/modules/ldap_groups/config/locales/crowdin/ru.yml @@ -3,11 +3,11 @@ ru: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP-подключение' + ldap_auth_source: 'Подключение к LDAP' sync_users: 'Синхронизация пользователей' ldap_groups/synchronized_filter: filter_string: 'Фильтр LDAP' - auth_source: 'LDAP-подключение' + ldap_auth_source: 'Подключение к LDAP' group_name_attribute: "Атрибут имени группы" sync_users: 'Синхронизация пользователей' base_dn: "Поиск базы DN" diff --git a/modules/ldap_groups/config/locales/crowdin/rw.yml b/modules/ldap_groups/config/locales/crowdin/rw.yml index 0eac7faf613..440707b6096 100644 --- a/modules/ldap_groups/config/locales/crowdin/rw.yml +++ b/modules/ldap_groups/config/locales/crowdin/rw.yml @@ -3,11 +3,11 @@ rw: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/si.yml b/modules/ldap_groups/config/locales/crowdin/si.yml index f9e83f48965..26004200f05 100644 --- a/modules/ldap_groups/config/locales/crowdin/si.yml +++ b/modules/ldap_groups/config/locales/crowdin/si.yml @@ -3,11 +3,11 @@ si: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/sk.yml b/modules/ldap_groups/config/locales/crowdin/sk.yml index b38062dd0ae..e0d4a49f148 100644 --- a/modules/ldap_groups/config/locales/crowdin/sk.yml +++ b/modules/ldap_groups/config/locales/crowdin/sk.yml @@ -3,11 +3,11 @@ sk: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'Filter LDAP' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/sl.yml b/modules/ldap_groups/config/locales/crowdin/sl.yml index 241b0d9e2ae..b88c14fb9db 100644 --- a/modules/ldap_groups/config/locales/crowdin/sl.yml +++ b/modules/ldap_groups/config/locales/crowdin/sl.yml @@ -3,11 +3,11 @@ sl: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP povezava' + ldap_auth_source: 'LDAP connection' sync_users: 'Sinhroniziraj uporabnike' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP povezava' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Atribut imena skupine\n" sync_users: 'Sinhroniziraj uporabnike' base_dn: "Išči po bazi DN" diff --git a/modules/ldap_groups/config/locales/crowdin/sr.yml b/modules/ldap_groups/config/locales/crowdin/sr.yml index fe0cb4a9360..6681a8942e0 100644 --- a/modules/ldap_groups/config/locales/crowdin/sr.yml +++ b/modules/ldap_groups/config/locales/crowdin/sr.yml @@ -3,11 +3,11 @@ sr: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/sv.yml b/modules/ldap_groups/config/locales/crowdin/sv.yml index 71b5a009477..6e7ead354c0 100644 --- a/modules/ldap_groups/config/locales/crowdin/sv.yml +++ b/modules/ldap_groups/config/locales/crowdin/sv.yml @@ -3,11 +3,11 @@ sv: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP-anslutning' + ldap_auth_source: 'LDAP connection' sync_users: 'Synkronisera användare' ldap_groups/synchronized_filter: filter_string: 'LDAP-filter' - auth_source: 'LDAP-anslutning' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Attribut för gruppnamn" sync_users: 'Synkronisera användare' base_dn: "Sök bas DN:" diff --git a/modules/ldap_groups/config/locales/crowdin/th.yml b/modules/ldap_groups/config/locales/crowdin/th.yml index 39616f1c714..5311c386a90 100644 --- a/modules/ldap_groups/config/locales/crowdin/th.yml +++ b/modules/ldap_groups/config/locales/crowdin/th.yml @@ -3,11 +3,11 @@ th: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/tr.yml b/modules/ldap_groups/config/locales/crowdin/tr.yml index 72a947f0252..87e8023cc94 100644 --- a/modules/ldap_groups/config/locales/crowdin/tr.yml +++ b/modules/ldap_groups/config/locales/crowdin/tr.yml @@ -3,11 +3,11 @@ tr: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP bağlantısı' + ldap_auth_source: 'LDAP connection' sync_users: 'Kullanıcıları senkronize et' ldap_groups/synchronized_filter: filter_string: 'LDAP süzgeçi' - auth_source: 'LDAP bağlantısı' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Grup adı özelliği" sync_users: 'Kullanıcıları senkronize et' base_dn: "Arama tabanı DN" diff --git a/modules/ldap_groups/config/locales/crowdin/uk.yml b/modules/ldap_groups/config/locales/crowdin/uk.yml index 1d244db32c0..d7e71ad0ef4 100644 --- a/modules/ldap_groups/config/locales/crowdin/uk.yml +++ b/modules/ldap_groups/config/locales/crowdin/uk.yml @@ -3,11 +3,11 @@ uk: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP-підключення' + ldap_auth_source: 'LDAP connection' sync_users: 'Синхронізація користувачів' ldap_groups/synchronized_filter: filter_string: 'Фільтр LDAP' - auth_source: 'LDAP-підключення' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Атрибут назви групи" sync_users: 'Синхронізація користувачів' base_dn: "База пошуку унікальних імен" diff --git a/modules/ldap_groups/config/locales/crowdin/vi.yml b/modules/ldap_groups/config/locales/crowdin/vi.yml index 337f5194836..49c834fe455 100644 --- a/modules/ldap_groups/config/locales/crowdin/vi.yml +++ b/modules/ldap_groups/config/locales/crowdin/vi.yml @@ -3,11 +3,11 @@ vi: attributes: ldap_groups/synchronized_group: dn: 'DN' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' sync_users: 'Sync users' ldap_groups/synchronized_filter: filter_string: 'LDAP filter' - auth_source: 'LDAP connection' + ldap_auth_source: 'LDAP connection' group_name_attribute: "Group name attribute" sync_users: 'Sync users' base_dn: "Search base DN" diff --git a/modules/ldap_groups/config/locales/crowdin/zh-TW.yml b/modules/ldap_groups/config/locales/crowdin/zh-TW.yml index 6147e9f7b34..c94b111f4e0 100644 --- a/modules/ldap_groups/config/locales/crowdin/zh-TW.yml +++ b/modules/ldap_groups/config/locales/crowdin/zh-TW.yml @@ -3,11 +3,11 @@ zh-TW: attributes: ldap_groups/synchronized_group: dn: '獨特名' - auth_source: 'LDAP 連線' + ldap_auth_source: 'LDAP 連線' sync_users: '同步使用者' ldap_groups/synchronized_filter: filter_string: '簡約登入目錄制約(LDAP)篩選' - auth_source: 'LDAP 連線' + ldap_auth_source: 'LDAP 連線' group_name_attribute: "群組名字屬性" sync_users: '同步使用者' base_dn: "搜尋基礎 DN" @@ -49,7 +49,7 @@ zh-TW: title: '移除同步的群組 %{name}' confirmation: "如繼續,將移除同步的群組 %{name} 和所有透過該群組同步的全部 %{users_count} 個用戶。" info: "Note: The OpenProject group itself and members added outside this LDAP synchronization will not be removed." - verification: "Enter the group's name %{name} to verify the deletion." + verification: "輸入專案的名稱 %{name} 來確認刪除" help_text_html: | This module allows you to set up a synchronization between LDAP and OpenProject groups. It depends on LDAP groups need to use the groupOfNames / memberOf attribute set to be working with OpenProject. diff --git a/modules/ldap_groups/lib/open_project/ldap_groups/patches/ldap_auth_source_patch.rb b/modules/ldap_groups/lib/open_project/ldap_groups/patches/ldap_auth_source_patch.rb index c8effc02dd5..b5137b1e17a 100644 --- a/modules/ldap_groups/lib/open_project/ldap_groups/patches/ldap_auth_source_patch.rb +++ b/modules/ldap_groups/lib/open_project/ldap_groups/patches/ldap_auth_source_patch.rb @@ -6,6 +6,10 @@ module OpenProject::LdapGroups has_many :ldap_groups_synchronized_groups, class_name: '::LdapGroups::SynchronizedGroup', dependent: :destroy + + has_many :ldap_groups_synchronized_filters, + class_name: '::LdapGroups::SynchronizedFilter', + dependent: :destroy end end end diff --git a/modules/ldap_groups/spec/features/filter_administration_spec.rb b/modules/ldap_groups/spec/features/filter_administration_spec.rb new file mode 100644 index 00000000000..1f5beada2e6 --- /dev/null +++ b/modules/ldap_groups/spec/features/filter_administration_spec.rb @@ -0,0 +1,48 @@ +require_relative '../spec_helper' + +RSpec.describe 'LDAP group filter administration spec', js: true do + let(:admin) { create(:admin) } + + before do + login_as admin + end + + context 'with EE', with_ee: %i[ldap_groups] do + context 'when providing seed variables', + :settings_reset, + with_env: { + OPENPROJECT_SEED_LDAP_FOO_HOST: "localhost", + OPENPROJECT_SEED_LDAP_FOO_PORT: "12389", + OPENPROJECT_SEED_LDAP_FOO_SECURITY: "plain_ldap", + OPENPROJECT_SEED_LDAP_FOO_TLS__VERIFY: "false", + OPENPROJECT_SEED_LDAP_FOO_BINDUSER: "uid=admin,ou=system", + OPENPROJECT_SEED_LDAP_FOO_BINDPASSWORD: "secret", + OPENPROJECT_SEED_LDAP_FOO_BASEDN: "dc=example,dc=com", + OPENPROJECT_SEED_LDAP_FOO_FILTER: "(uid=*)", + OPENPROJECT_SEED_LDAP_FOO_SYNC__USERS: "true", + OPENPROJECT_SEED_LDAP_FOO_LOGIN__MAPPING: "uid", + OPENPROJECT_SEED_LDAP_FOO_FIRSTNAME__MAPPING: "givenName", + OPENPROJECT_SEED_LDAP_FOO_LASTNAME__MAPPING: "sn", + OPENPROJECT_SEED_LDAP_FOO_MAIL__MAPPING: "mail", + OPENPROJECT_SEED_LDAP_FOO_ADMIN__MAPPING: "", + OPENPROJECT_SEED_LDAP_FOO_GROUPFILTER_BAR_BASE: "ou=groups,dc=example,dc=com", + OPENPROJECT_SEED_LDAP_FOO_GROUPFILTER_BAR_FILTER: "(cn=*)", + OPENPROJECT_SEED_LDAP_FOO_GROUPFILTER_BAR_SYNC__USERS: "true", + OPENPROJECT_SEED_LDAP_FOO_GROUPFILTER_BAR_GROUP__ATTRIBUTE: "dn" + } do + it 'blocks editing of the filter' do + reset(:seed_ldap) + allow(LdapGroups::SynchronizationJob).to receive(:perform_now) + EnvData::LdapSeeder.new({}).seed_data! + + visit ldap_groups_synchronized_groups_path + expect(page).to have_text 'bar' + page.find('td.name a', text: 'bar').click + + expect(page).to have_text I18n.t(:label_seeded_from_env_warning) + expect(page).not_to have_link 'Edit' + expect(page).not_to have_link 'Delete' + end + end + end +end diff --git a/modules/meeting/app/controllers/meetings_controller.rb b/modules/meeting/app/controllers/meetings_controller.rb index 57c6481959c..754dc5ce55e 100644 --- a/modules/meeting/app/controllers/meetings_controller.rb +++ b/modules/meeting/app/controllers/meetings_controller.rb @@ -32,8 +32,8 @@ class MeetingsController < ApplicationController before_action :build_meeting, only: %i[new create] before_action :find_meeting, except: %i[index new create] before_action :convert_params, only: %i[create update] - before_action :authorize, except: %i[index new] - before_action :authorize_global, only: %i[index new] + before_action :authorize, except: %i[index new create] + before_action :authorize_global, only: %i[index new create] helper :watchers helper :meeting_contents diff --git a/modules/meeting/app/helpers/meetings_helper.rb b/modules/meeting/app/helpers/meetings_helper.rb index acd2a3b5e1a..b1e123d093a 100644 --- a/modules/meeting/app/helpers/meetings_helper.rb +++ b/modules/meeting/app/helpers/meetings_helper.rb @@ -171,7 +171,15 @@ module MeetingsHelper content_tag('div', "#{header}#{details}".html_safe, id: "change-#{journal.id}", class: 'journal') end - def global_create_context? + def global_meeting_create_context? + global_new_meeting_action? || global_create_meeting_action? + end + + def global_new_meeting_action? request.path == new_meeting_path end + + def global_create_meeting_action? + request.path == meetings_path && @project.nil? + end end diff --git a/modules/meeting/app/views/meetings/_form.html.erb b/modules/meeting/app/views/meetings/_form.html.erb index 11738e292f3..14902e91bca 100644 --- a/modules/meeting/app/views/meetings/_form.html.erb +++ b/modules/meeting/app/views/meetings/_form.html.erb @@ -35,7 +35,7 @@ See COPYRIGHT and LICENSE files for more details. <%= f.text_field :title, :required => true, :size => 60, container_class: '-wide' %>
    - <% if global_create_context? %> + <% if global_meeting_create_context? %>
    diff --git a/modules/meeting/config/locales/crowdin/cs.yml b/modules/meeting/config/locales/crowdin/cs.yml index feec646a53d..4939b41a8ab 100644 --- a/modules/meeting/config/locales/crowdin/cs.yml +++ b/modules/meeting/config/locales/crowdin/cs.yml @@ -93,7 +93,7 @@ cs: text_duration_in_hours: "Doba trvání v hodinách" text_in_hours: "v hodinách" text_meeting_agenda_for_meeting: 'Agenda schůzky "%{meeting}"' - text_meeting_closing_are_you_sure: "Are you sure you want to close the meeting agenda?" + text_meeting_closing_are_you_sure: "Jste si jisti, že chcete ukončit program schůzky?" text_meeting_agenda_open_are_you_sure: "Toto přepíše všechny změny v zápisech! Chcete pokračovat?" text_meeting_minutes_for_meeting: 'zápis pro schůzku "%{meeting}"' text_review_meeting_agenda: "%{author} dal %{link} na revizi." diff --git a/modules/meeting/config/locales/crowdin/it.yml b/modules/meeting/config/locales/crowdin/it.yml index 6515720f2d1..ae3dda2c41d 100644 --- a/modules/meeting/config/locales/crowdin/it.yml +++ b/modules/meeting/config/locales/crowdin/it.yml @@ -29,7 +29,7 @@ it: participants: "Partecipanti" participants_attended: "Invitati" participants_invited: "Invitati" - project: "Project" + project: "Progetto" start_time: "Tempo" start_time_hour: "Ora di inizio" errors: @@ -67,7 +67,7 @@ it: label_involvement: "Involvement" label_upcoming_invitations: "Upcoming invitations" label_past_invitations: "Past invitations" - label_attendee: "Attendee" + label_attendee: "Presenti" label_author: "Creator" label_notify: "Invia per revisione" label_icalendar: "Invia iCalendar" diff --git a/modules/meeting/config/locales/crowdin/nl.yml b/modules/meeting/config/locales/crowdin/nl.yml index 8138478493b..af34018efbc 100644 --- a/modules/meeting/config/locales/crowdin/nl.yml +++ b/modules/meeting/config/locales/crowdin/nl.yml @@ -62,13 +62,13 @@ nl: label_meeting_agenda_close: "Sluit de agenda om de notulen te beginnen" label_meeting_date_time: "Datum/Tijd" label_meeting_diff: "Diff" - label_upcoming_meetings: "Upcoming meetings" - label_past_meetings: "Past meetings" - label_involvement: "Involvement" - label_upcoming_invitations: "Upcoming invitations" - label_past_invitations: "Past invitations" - label_attendee: "Attendee" - label_author: "Creator" + label_upcoming_meetings: "Geplande vergaderingen" + label_past_meetings: "Eerdere vergaderingen" + label_involvement: "Betrokkenheid" + label_upcoming_invitations: "Aankomende uitnodigingen" + label_past_invitations: "Vorige uitnodigingen" + label_attendee: "Deelnemer" + label_author: "Maker" label_notify: "Verzenden voor revisie" label_icalendar: "Verstuur iCalendar" label_version: "Versie" @@ -93,7 +93,7 @@ nl: text_duration_in_hours: "Duur in uren" text_in_hours: "in uren" text_meeting_agenda_for_meeting: 'agenda voor de vergadering "%{meeting}"' - text_meeting_closing_are_you_sure: "Are you sure you want to close the meeting agenda?" + text_meeting_closing_are_you_sure: "Weet je zeker dat je de vergaderagenda wilt sluiten?" text_meeting_agenda_open_are_you_sure: "Dit overschrijft alle wijzigingen in de aantekeningen! Wilt u doorgaan?" text_meeting_minutes_for_meeting: 'minuten voor de vergadering "%{meeting}"' text_review_meeting_agenda: "%{author} heeft de %{link} geselecteerd voor herziening." diff --git a/modules/meeting/lib/open_project/meeting/engine.rb b/modules/meeting/lib/open_project/meeting/engine.rb index 59680de3d48..07f3d07db63 100644 --- a/modules/meeting/lib/open_project/meeting/engine.rb +++ b/modules/meeting/lib/open_project/meeting/engine.rb @@ -108,8 +108,6 @@ module OpenProject::Meeting patches [:Project] patch_with_namespace :BasicData, :SettingSeeder - patch_with_namespace :OpenProject, :TextFormatting, :Formats, :Markdown, :TextileConverter - add_api_endpoint 'API::V3::Root' do mount ::API::V3::Meetings::MeetingContentsAPI end diff --git a/modules/meeting/spec/controllers/meetings_controller_spec.rb b/modules/meeting/spec/controllers/meetings_controller_spec.rb index 7a0528ded4b..93f9c15e731 100644 --- a/modules/meeting/spec/controllers/meetings_controller_spec.rb +++ b/modules/meeting/spec/controllers/meetings_controller_spec.rb @@ -135,53 +135,90 @@ RSpec.describe MeetingsController do it { expect(assigns(:meeting)).to eql meeting } end end + end + describe 'POST' do describe 'create' do render_views + let(:base_params) do + { + project_id: project&.id, + meeting: meeting_params + } + end + + let(:base_meeting_params) do + { + title: 'Foobar', + duration: '1.0', + start_date: '2015-06-01', + start_time_hour: '10:00' + } + end + + let(:params) { base_params } + let(:meeting_params) { base_meeting_params } + before do allow(Project).to receive(:find).and_return(project) + post :create, - params: { - project_id: project.id, - meeting: { - title: 'Foobar', - duration: '1.0' - }.merge(params) - } + params: end - describe 'invalid start_date' do - let(:params) do - { - start_date: '-', - start_time_hour: '10:00' - } + context 'with a project_id' do + context 'and an invalid start_date with start_time_hour' do + let(:meeting_params) do + base_meeting_params.merge(start_date: '-') + end + + it 'renders an error' do + expect(response).to have_http_status :ok + expect(response).to render_template :new + expect(response.body) + .to have_selector '#errorExplanation li', + text: "Start date #{I18n.t('activerecord.errors.messages.not_an_iso_date')}" + end end + context 'and an invalid start_time_hour with start_date' do + let(:meeting_params) do + base_meeting_params.merge(start_time_hour: '-') + end + + it 'renders an error' do + expect(response).to have_http_status :ok + expect(response).to render_template :new + expect(response.body) + .to have_selector '#errorExplanation li', + text: "Starting time #{I18n.t('activerecord.errors.messages.invalid_time_format')}" + end + end + end + + context 'with a nil project_id' do + let(:project) { nil } + it 'renders an error' do expect(response).to have_http_status :ok expect(response).to render_template :new expect(response.body) .to have_selector '#errorExplanation li', - text: "Start date #{I18n.t('activerecord.errors.messages.not_an_iso_date')}" + text: "Project #{I18n.t('activerecord.errors.messages.blank')}" end end - describe 'invalid start_time_hour' do - let(:params) do - { - start_date: '2015-06-01', - start_time_hour: '-' - } - end + context 'without a project_id' do + let(:params) { base_params.except(:project_id) } + let(:project) { nil } it 'renders an error' do expect(response).to have_http_status :ok expect(response).to render_template :new expect(response.body) .to have_selector '#errorExplanation li', - text: "Starting time #{I18n.t('activerecord.errors.messages.invalid_time_format')}" + text: "Project #{I18n.t('activerecord.errors.messages.blank')}" end end end diff --git a/modules/meeting/spec/features/meetings_new_spec.rb b/modules/meeting/spec/features/meetings_new_spec.rb index b4dfec2115e..0bcae2a8b9d 100644 --- a/modules/meeting/spec/features/meetings_new_spec.rb +++ b/modules/meeting/spec/features/meetings_new_spec.rb @@ -30,7 +30,7 @@ require 'spec_helper' require_relative '../support/pages/meetings/index' -RSpec.describe 'Meetings new', :js do +RSpec.describe 'Meetings new', :js, with_cuprite: false do shared_let(:project) { create(:project, enabled_module_names: %w[meetings]) } shared_let(:admin) { create(:admin) } let(:time_zone) { 'utc' } @@ -64,19 +64,29 @@ RSpec.describe 'Meetings new', :js do end let(:index_page) { Pages::Meetings::Index.new(project: nil) } + let(:new_page) { Pages::Meetings::New.new(nil) } context 'with permission to create meetings' do - it 'does not render menus' do - index_page.expect_no_main_menu + it 'does not render menus', :with_cuprite do + new_page.visit! + new_page.expect_no_main_menu + end + + describe 'clicking on the create new meeting button', :with_cuprite do + it 'navigates to the global create form' do + index_page.visit! + index_page.click_create_new + expect(page).to have_current_path(new_page.path) + end end ['CET', 'UTC', '', 'Pacific Time (US & Canada)'].each do |zone| let(:time_zone) { zone } - it "allows creating a project and handles errors in time zone #{zone}", with_cuprite: false do - index_page.visit! + it "allows creating a project and handles errors in time zone #{zone}" do + new_page.visit! - new_page = index_page.click_create_new + expect_angular_frontend_initialized # Wait for project dropdown to be ready new_page.set_title 'Some title' new_page.set_project project @@ -95,9 +105,53 @@ RSpec.describe 'Meetings new', :js do show_page.expect_date_time "03/28/2013 01:30 PM - 03:00 PM" end end + + context 'without a title set' do + before do + new_page.visit! + + # Wait for project dropdown to be initialized + expect_angular_frontend_initialized + + new_page.set_project project + + new_page.set_start_date '2013-03-28' + new_page.set_start_time '13:30' + new_page.set_duration '1.5' + new_page.invite(other_user) + end + + it 'renders a validation error' do + expect do + new_page.click_create + end.not_to change(Query, :count) + + # HTML required attribute validation error + expect(page).to have_current_path(new_page.path) + end + end + + context 'without a project set' do + before do + new_page.visit! + new_page.set_title 'Some title' + new_page.set_start_date '2013-03-28' + new_page.set_start_time '13:30' + new_page.set_duration '1.5' + end + + it 'renders a validation error' do + new_page.click_create + + new_page.expect_toast(message: "#{Project.model_name.human} #{I18n.t('activerecord.errors.messages.blank')}", + type: :error) + + new_page.expect_project_dropdown + end + end end - context 'without permission to create meetings' do + context 'without permission to create meetings', :with_cuprite do let(:permissions) { %i[view_meetings] } it 'shows no edit link' do @@ -107,13 +161,14 @@ RSpec.describe 'Meetings new', :js do end end - context 'as an admin' do + context 'as an admin', :with_cuprite do let(:current_user) { admin } - it 'allows creating meeting in a project without members', with_cuprite: false do - index_page.visit! + it 'allows creating meeting in a project without members' do + new_page.visit! + + expect_angular_frontend_initialized # Wait for project dropdown to be ready - new_page = index_page.click_create_new new_page.set_title 'Some title' new_page.set_project project @@ -125,24 +180,66 @@ RSpec.describe 'Meetings new', :js do # Not sure if that is then intended behaviour but that is what is currently programmed show_page.expect_invited(admin) end + + context 'without a project set' do + before do + new_page.visit! + new_page.set_title 'Some title' + end + + it 'renders a validation error' do + new_page.click_create + + new_page.expect_toast(message: "#{Project.model_name.human} #{I18n.t('activerecord.errors.messages.blank')}", + type: :error) + new_page.expect_project_dropdown + end + end + + context 'without a title set' do + before do + new_page.visit! + + # Wait for project dropdown to be initialized + expect_angular_frontend_initialized + + new_page.set_project project + end + + it 'renders a validation error' do + expect do + new_page.click_create + end.not_to change(Query, :count) + + # HTML required attribute validation error + expect(page).to have_current_path(new_page.path) + end + end end end context 'when creating a meeting from the project-specific page' do let(:index_page) { Pages::Meetings::Index.new(project:) } + let(:new_page) { Pages::Meetings::New.new(project) } context 'with permission to create meetings' do before do other_user end + describe 'clicking on the create new meeting button', :with_cuprite do + it 'navigates to the project-specific create form' do + index_page.visit! + index_page.click_create_new + expect(page).to have_current_path(new_page.path) + end + end + ['CET', 'UTC', '', 'Pacific Time (US & Canada)'].each do |zone| let(:time_zone) { zone } - it "allows creating a project and handles errors in time zone #{zone}", with_cuprite: false do - index_page.visit! - - new_page = index_page.click_create_new + it "allows creating a project and handles errors in time zone #{zone}" do + new_page.visit! new_page.set_title 'Some title' new_page.set_start_date '2013-03-28' @@ -159,9 +256,28 @@ RSpec.describe 'Meetings new', :js do show_page.expect_date_time "03/28/2013 01:30 PM - 03:00 PM" end end + + context 'without a title set' do + before do + new_page.visit! + new_page.set_start_date '2013-03-28' + new_page.set_start_time '13:30' + new_page.set_duration '1.5' + new_page.invite(other_user) + end + + it 'renders a validation error' do + expect do + new_page.click_create + end.not_to change(Query, :count) + + # HTML required attribute validation error + expect(page).to have_current_path(new_page.path) + end + end end - context 'without permission to create meetings' do + context 'without permission to create meetings', :with_cuprite do let(:permissions) { %i[view_meetings] } it 'shows no edit link' do @@ -171,13 +287,16 @@ RSpec.describe 'Meetings new', :js do end end - context 'as an admin' do + context 'as an admin', :with_cuprite do let(:current_user) { admin } + let(:field) do + TextEditorField.new(page, + '', + selector: '[data-qa-selector="op-meeting--meeting_agenda"]') + end it 'allows creating meeting in a project without members' do - index_page.visit! - - new_page = index_page.click_create_new + new_page.visit! new_page.set_title 'Some title' @@ -188,6 +307,43 @@ RSpec.describe 'Meetings new', :js do # Not sure if that is then intended behaviour but that is what is currently programmed show_page.expect_invited(admin) end + + context 'without a title set' do + before do + new_page.visit! + end + + it 'renders a validation error' do + expect do + new_page.click_create + end.not_to change(Query, :count) + + # HTML required attribute validation error + expect(page).to have_current_path(new_page.path) + end + end + + it 'can save the meeting agenda via cmd+Enter' do + new_page.visit! + + new_page.set_title 'Some title' + + show_page = new_page.click_create + + show_page.expect_toast(message: 'Successful creation') + + meeting = Meeting.last + + field.set_value('My new meeting text') + + field.submit_by_enter + + show_page.expect_and_dismiss_toaster message: 'Successful update' + + meeting.reload + + expect(meeting.agenda.text).to eq 'My new meeting text' + end end end end diff --git a/modules/meeting/spec/support/pages/meetings/new.rb b/modules/meeting/spec/support/pages/meetings/new.rb index 042828e2989..528552fedc2 100644 --- a/modules/meeting/spec/support/pages/meetings/new.rb +++ b/modules/meeting/spec/support/pages/meetings/new.rb @@ -33,6 +33,10 @@ module Pages::Meetings class New < Base include Components::Autocompleter::NgSelectAutocompleteHelpers + def expect_no_main_menu + expect(page).not_to have_selector '#main-menu' + end + def click_create click_button 'Create' @@ -49,8 +53,12 @@ module Pages::Meetings fill_in 'Title', with: text end + def expect_project_dropdown + find "[data-qa-selector='project_id']" + end + def set_project(project) - select_autocomplete find("[data-qa-selector='project_id'"), + select_autocomplete find("[data-qa-selector='project_id']"), query: project.name, results_selector: 'body' end diff --git a/modules/overviews/lib/overviews/engine.rb b/modules/overviews/lib/overviews/engine.rb index f8107a80f5b..8c4caf4778b 100644 --- a/modules/overviews/lib/overviews/engine.rb +++ b/modules/overviews/lib/overviews/engine.rb @@ -30,8 +30,6 @@ module Overviews end end - patch_with_namespace :OpenProject, :TextFormatting, :Formats, :Markdown, :TextileConverter - initializer 'overviews.conversion' do require Rails.root.join('config/constants/ar_to_api_conversions') diff --git a/modules/overviews/lib/overviews/patches/textile_converter_patch.rb b/modules/overviews/lib/overviews/patches/textile_converter_patch.rb deleted file mode 100644 index a17183a8a9e..00000000000 --- a/modules/overviews/lib/overviews/patches/textile_converter_patch.rb +++ /dev/null @@ -1,43 +0,0 @@ -# As the successor to the MyProjectPage Plugin and possibly inheriting the data from it, -# we Overview needs to convert textile in custom_text widgets. The converter can be executed after -# the migration. - -module Overviews::Patches - module TextileConverterPatch - extend ActiveSupport::Concern - - included do - prepend(Patch) - end - - module Patch - def convert_custom_text_widgets - # If the table does not exist yet, there is nothing to convert - return unless Grids::Overview.table_exists? - - print Grids::Overview.name - - text_widgets_to_convert - .in_batches(of: 200) do |widget| - widget.options['text'] = convert_textile_to_markdown(widget.options['text']) if widget.options['text'] - widget.save - - print ' .' - end - - print 'done' - end - - def text_widgets_to_convert - Grids::Widget - .includes(:grid) - .where(grids: { type: 'Grids::Overview' }) - .where(identifier: 'custom_text') - end - - def converters - super + [method(:convert_custom_text_widgets)] - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_file_info_converter.rb b/modules/storages/app/common/storages/peripherals/storage_file_info_converter.rb new file mode 100644 index 00000000000..4bff762dd45 --- /dev/null +++ b/modules/storages/app/common/storages/peripherals/storage_file_info_converter.rb @@ -0,0 +1,46 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Storages::Peripherals + module StorageFileInfoConverter + def to_storage_file(storage_file_info) + Storages::StorageFile.new( + id: storage_file_info.id, + name: storage_file_info.name, + size: storage_file_info.size, + mime_type: storage_file_info.mime_type, + created_at: storage_file_info.created_at, + last_modified_at: storage_file_info.last_modified_at, + created_by_name: storage_file_info.owner_name, + last_modified_by_name: storage_file_info.last_modified_by_name, + location: storage_file_info.location, + permissions: storage_file_info.permissions + ) + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_ids_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_ids_query.rb new file mode 100644 index 00000000000..ce969dbc05d --- /dev/null +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_ids_query.rb @@ -0,0 +1,45 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Storages::Peripherals::StorageInteraction::Nextcloud + class FileIdsQuery + def initialize(storage) + @query = ::Storages::Peripherals::StorageInteraction::Nextcloud::Internal::PropfindQuery.new(storage) + end + + def call(path:) + query_params = { + depth: '1', + path:, + props: %w[oc:fileid] + } + + @query.call(**query_params) + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_query.rb deleted file mode 100644 index 45211ce025b..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_query.rb +++ /dev/null @@ -1,118 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2023 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module Storages::Peripherals::StorageInteraction::Nextcloud - class FileQuery - using Storages::Peripherals::ServiceResultRefinements - - FILE_INFO_PATH = 'ocs/v1.php/apps/integration_openproject/fileinfo'.freeze - - def initialize(storage) - @uri = URI(storage.host).normalize - @oauth_client = storage.oauth_client - end - - def call(user:, file_id:) - Util.token(user:, oauth_client: @oauth_client) do |token| - file_info(file_id, token) >> - method(:handle_access_control) >> - method(:storage_file) - end - end - - def error(code, log_message = nil, data = nil) - ServiceResult.failure(errors: Storages::StorageError.new(code:, log_message:, data:)) - end - - private - - def file_info(file_id, token) - service_result = begin - ServiceResult.success( - result: RestClient::Request.execute( - method: :get, - url: Util.join_uri_path(@uri, FILE_INFO_PATH, file_id), - headers: { - 'Authorization' => "Bearer #{token.access_token}", - 'Accept' => 'application/json', - 'Content-Type' => 'application/json' - } - ) - ) - rescue RestClient::Unauthorized => e - error(:not_authorized, 'Outbound request not authorized!', e.response) - rescue RestClient::NotFound => e - Util.error(:not_found, 'Outbound request destination not found!', e.response) - rescue RestClient::ExceptionWithResponse => e - Util.error(:error, 'Outbound request failed!', e.response) - rescue StandardError - Util.error(:error, 'Outbound request failed!') - end - - # rubocop:disable Style/OpenStructUse - service_result.map { |response| JSON.parse(response.body, object_class: OpenStruct) } - # rubocop:enable Style/OpenStructUse - end - - def handle_access_control(file_info_response) - data = file_info_response.ocs.data - if data.statuscode == 403 - Util.error(:forbidden) - else - ServiceResult.success(result: data) - end - end - - # rubocop:disable Metrics/AbcSize - def storage_file(file_info_data) - storage_file = ::Storages::StorageFile.new(file_info_data.id, - file_info_data.name, - file_info_data.size, - file_info_data.mimetype, - Time.zone.at(file_info_data.ctime), - Time.zone.at(file_info_data.mtime), - file_info_data.owner_name, - file_info_data.modifier_name, - location(file_info_data.path), - file_info_data.dav_permissions) - ServiceResult.success(result: storage_file) - end - - # rubocop:enable Metrics/AbcSize - - def location(files_path) - prefix = 'files/' - idx = files_path.rindex(prefix) - return '/' if idx == nil - - idx += prefix.length - 1 - - Util.escape_path(files_path[idx..]) - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/files_info_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/files_info_query.rb new file mode 100644 index 00000000000..9fe5ac3ddef --- /dev/null +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/files_info_query.rb @@ -0,0 +1,145 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Storages::Peripherals::StorageInteraction::Nextcloud + class FilesInfoQuery + using Storages::Peripherals::ServiceResultRefinements + + FILES_INFO_PATH = 'ocs/v1.php/apps/integration_openproject/filesinfo'.freeze + + def initialize(storage) + @uri = URI(storage.host).normalize + @oauth_client = storage.oauth_client + end + + def call(user:, file_ids: []) + if file_ids.nil? + return Util.error(:error, 'File IDs can not be nil', file_ids) + end + + if file_ids.empty? + return ServiceResult.success(result: []) + end + + Util.token(user:, oauth_client: @oauth_client) do |token| + files_info(file_ids, token).map(&parse_json) >> handle_failure >> create_storage_file_infos + end + end + + private + + def files_info(file_ids, token) + ServiceResult.success( + result: RestClient::Request.execute( + method: :post, + url: Util.join_uri_path(@uri, FILES_INFO_PATH), + payload: { fileIds: file_ids }.to_json, + headers: { + 'Authorization' => "Bearer #{token.access_token}", + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'OCS-APIRequest' => true + } + ) + ).map(&:body) + rescue RestClient::Unauthorized => e + Util.error(:not_authorized, 'Outbound request not authorized!', e.response) + rescue RestClient::NotFound => e + Util.error(:not_found, 'Outbound request destination not found!', e.response) + rescue RestClient::ExceptionWithResponse => e + Util.error(:error, 'Outbound request failed!', e.response) + rescue StandardError + Util.error(:error, 'Outbound request failed!') + end + + def parse_json + ->(response_body) do + # rubocop:disable Style/OpenStructUse + JSON.parse(response_body, object_class: OpenStruct) + # rubocop:enable Style/OpenStructUse + end + end + + def handle_failure + ->(response_object) do + if response_object.ocs.meta.status == 'ok' + ServiceResult.success(result: response_object) + else + Util.error(:error, 'Outbound request failed!', response_object) + end + end + end + + # rubocop:disable Metrics/AbcSize + def create_storage_file_infos + ->(response_object) do + ServiceResult.success( + result: response_object.ocs.data.each_pair.map do |key, value| + if value.statuscode == 200 + ::Storages::StorageFileInfo.new( + status: value.status, + status_code: value.statuscode, + id: value.id, + name: value.name, + last_modified_at: Time.zone.at(value.mtime), + created_at: Time.zone.at(value.ctime), + mime_type: value.mimetype, + size: value.size, + owner_name: value.owner_name, + owner_id: value.owner_id, + trashed: value.trashed, + last_modified_by_name: value.modifier_name, + last_modified_by_id: value.modifier_id, + permissions: value.dav_permissions, + location: location(value.path) + ) + else + ::Storages::StorageFileInfo.new( + status: value.status, + status_code: value.statuscode, + id: key.to_s.to_i + ) + end + end + ) + end + end + + # rubocop:enable Metrics/AbcSize + + def location(file_path) + prefix = 'files/' + idx = file_path.rindex(prefix) + return '/' if idx == nil + + idx += prefix.length - 1 + + Util.escape_path(file_path[idx..]) + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/files_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/files_query.rb index 1cfe2485cdd..e1b0f255be0 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/files_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/files_query.rb @@ -124,16 +124,7 @@ module Storages::Peripherals::StorageInteraction::Nextcloud # The ancestors are simply derived objects from the parents location string. Until we have real information # from the nextcloud API about the path to the parent, we need to derive name, location and forge an ID. def forge_ancestor(location) - ::Storages::StorageFile.new(Digest::SHA256.hexdigest(location), - name(location), - nil, - nil, - nil, - nil, - nil, - nil, - location, - nil) + ::Storages::StorageFile.new(id: Digest::SHA256.hexdigest(location), name: name(location), location:) end def name(location) @@ -144,16 +135,14 @@ module Storages::Peripherals::StorageInteraction::Nextcloud location = location(file_element) ::Storages::StorageFile.new( - id(file_element), - name(location), - size(file_element), - mime_type(file_element), - nil, - last_modified_at(file_element), - created_by(file_element), - nil, - location, - permissions(file_element) + id: id(file_element), + name: name(location), + size: size(file_element), + mime_type: mime_type(file_element), + last_modified_at: last_modified_at(file_element), + created_by_name: created_by(file_element), + location:, + permissions: permissions(file_element) ) end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query.rb new file mode 100644 index 00000000000..1b71a7b28b2 --- /dev/null +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/folder_files_file_ids_deep_query.rb @@ -0,0 +1,43 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Storages::Peripherals::StorageInteraction::Nextcloud + class FolderFilesFileIdsDeepQuery + def initialize(storage) + @query = ::Storages::Peripherals::StorageInteraction::Nextcloud::Internal::PropfindQuery.new(storage) + end + + def call(path:) + @query.call( + depth: 'infinity', + path:, + props: %w[oc:fileid] + ) + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/propfind_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/propfind_query.rb similarity index 82% rename from modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/propfind_query.rb rename to modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/propfind_query.rb index 5cc95e27b65..cda875bdb81 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/propfind_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/propfind_query.rb @@ -26,8 +26,10 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Storages::Peripherals::StorageInteraction::Nextcloud +module Storages::Peripherals::StorageInteraction::Nextcloud::Internal class PropfindQuery + UTIL = ::Storages::Peripherals::StorageInteraction::Nextcloud::Util + # Only for information purposes currently. # Probably a bit later we could validate `#call` parameters. # @@ -82,10 +84,15 @@ module Storages::Peripherals::StorageInteraction::Nextcloud end end.to_xml - response = Util.http(@uri).propfind( - Util.join_uri_path(@uri, 'remote.php/dav/files', CGI.escapeURIComponent(@username), Util.escape_path(path)), + response = UTIL.http(@uri).propfind( + UTIL.join_uri_path( + @uri, + 'remote.php/dav/files', + CGI.escapeURIComponent(@username), + UTIL.escape_path(path) + ), body, - Util.basic_auth_header(@username, @password).merge('Depth' => depth) + UTIL.basic_auth_header(@username, @password).merge('Depth' => depth) ) case response @@ -93,9 +100,9 @@ module Storages::Peripherals::StorageInteraction::Nextcloud doc = Nokogiri::XML response.body result = {} doc.xpath('/d:multistatus/d:response').each do |resource_section| - resource = CGI - .unescape(resource_section.xpath("d:href").text.strip) - .gsub!(Util.join_uri_path(@uri.path, "/remote.php/dav/files/#{@username}/"), "") + resource = CGI.unescape(resource_section.xpath("d:href").text.strip) + .gsub!(UTIL.join_uri_path(@uri.path, "/remote.php/dav/files/#{@username}/"), "") + result[resource] = {} # In future it could be useful to respond not only with found, but not found props as well @@ -104,17 +111,19 @@ module Storages::Peripherals::StorageInteraction::Nextcloud result[resource][node.name.to_s] = node.text.strip end end + ServiceResult.success(result:) when Net::HTTPMethodNotAllowed - Util.error(:not_allowed) + UTIL.error(:not_allowed) when Net::HTTPUnauthorized - Util.error(:not_authorized) + UTIL.error(:not_authorized) when Net::HTTPNotFound - Util.error(:not_found) + UTIL.error(:not_found) else - Util.error(:error) + UTIL.error(:error) end end + # rubocop:enable Metrics/AbcSize end end diff --git a/modules/storages/app/common/storages/peripherals/storage_requests.rb b/modules/storages/app/common/storages/peripherals/storage_requests.rb index 12aafaf70b3..9bdaed7341c 100644 --- a/modules/storages/app/common/storages/peripherals/storage_requests.rb +++ b/modules/storages/app/common/storages/peripherals/storage_requests.rb @@ -40,11 +40,12 @@ module Storages::Peripherals QUERIES = %i[ download_link_query - file_query + files_info_query files_query + file_ids_query + folder_files_file_ids_deep_query upload_link_query group_users_query - propfind_query ].freeze def initialize(storage:) diff --git a/modules/storages/app/components/storages/projects_storages/row_component.rb b/modules/storages/app/components/storages/project_storages/row_component.rb similarity index 82% rename from modules/storages/app/components/storages/projects_storages/row_component.rb rename to modules/storages/app/components/storages/project_storages/row_component.rb index 1d184bd5ca4..1bae4a4a9e2 100644 --- a/modules/storages/app/components/storages/projects_storages/row_component.rb +++ b/modules/storages/app/components/storages/project_storages/row_component.rb @@ -28,7 +28,7 @@ # Purpose: Defines how to format the components within a table row of ProjectStorages # associated with a project -module Storages::ProjectsStorages +module Storages::ProjectStorages class RowComponent < ::RowComponent def project_storage row @@ -50,21 +50,19 @@ module Storages::ProjectsStorages end def button_links - [delete_link].tap do |links| - links.unshift edit_link if OpenProject::FeatureDecisions.storage_project_folders_active? - end + [edit_link, delete_link] end def edit_link link_to '', - edit_project_settings_projects_storage_path(project_id: project_storage.project, id: project_storage), + edit_project_settings_project_storage_path(project_id: project_storage.project, id: project_storage), class: 'icon icon-edit', title: I18n.t(:button_edit) end def delete_link link_to '', - confirm_destroy_project_settings_projects_storage_path(project_id: project_storage.project, id: project_storage), + confirm_destroy_project_settings_project_storage_path(project_id: project_storage.project, id: project_storage), class: 'icon icon-delete', title: I18n.t(:button_delete), method: :get diff --git a/modules/storages/app/components/storages/projects_storages/table_component.rb b/modules/storages/app/components/storages/project_storages/table_component.rb similarity index 96% rename from modules/storages/app/components/storages/projects_storages/table_component.rb rename to modules/storages/app/components/storages/project_storages/table_component.rb index e371457ccf2..4c58c59403b 100644 --- a/modules/storages/app/components/storages/projects_storages/table_component.rb +++ b/modules/storages/app/components/storages/project_storages/table_component.rb @@ -31,7 +31,7 @@ # page. # See also: row_component.rb, which contains a method # for every "column" defined below. -module Storages::ProjectsStorages +module Storages::ProjectStorages class TableComponent < ::TableComponent columns :name, :provider_type, @@ -47,7 +47,7 @@ module Storages::ProjectsStorages end def inline_create_link - link_to(new_project_settings_projects_storage_path, + link_to(new_project_settings_project_storage_path, class: 'wp-inline-create--add-link', title: I18n.t('storages.label_new_storage')) do helpers.op_icon('icon icon-add') diff --git a/modules/storages/app/contracts/storages/file_links/create_contract.rb b/modules/storages/app/contracts/storages/file_links/create_contract.rb index ae8529dd6f7..d59936d4467 100644 --- a/modules/storages/app/contracts/storages/file_links/create_contract.rb +++ b/modules/storages/app/contracts/storages/file_links/create_contract.rb @@ -58,12 +58,11 @@ class Storages::FileLinks::CreateContract < ModelContract end end - # Check that the current has the permission on the project. - # model variable is available because the concern is executed inside a contract. def validate_user_allowed_to_manage - return if model.container.nil? || user.allowed_to?(:manage_file_links, model.project) - - errors.add(:base, :error_unauthorized) + container = model.container + if container.present? && !user.allowed_to?(:manage_file_links, container.project) + errors.add(:base, :error_unauthorized) + end end def validate_storage_presence diff --git a/modules/storages/app/contracts/storages/last_project_folders/base_contract.rb b/modules/storages/app/contracts/storages/last_project_folders/base_contract.rb index a3a2ce79c6f..e49fbc4e973 100644 --- a/modules/storages/app/contracts/storages/last_project_folders/base_contract.rb +++ b/modules/storages/app/contracts/storages/last_project_folders/base_contract.rb @@ -30,8 +30,8 @@ module Storages::LastProjectFolders class BaseContract < ::ModelContract include ActiveModel::Validations - attribute :projects_storage - validates_presence_of :projects_storage + attribute :project_storage + validates_presence_of :project_storage attribute :mode validates :mode, presence: true, inclusion: { in: %w[manual automatic] } attribute :origin_folder_id diff --git a/modules/storages/app/contracts/storages/project_storages/base_contract.rb b/modules/storages/app/contracts/storages/project_storages/base_contract.rb index f799642c778..08faa3abed1 100644 --- a/modules/storages/app/contracts/storages/project_storages/base_contract.rb +++ b/modules/storages/app/contracts/storages/project_storages/base_contract.rb @@ -28,7 +28,7 @@ # A "contract" is an OpenProject pattern used to validate parameters # before actually creating, updating, or deleting a model. -# Used by: projects_storages_controller.rb and in the API +# Used by: project_storages_controller.rb and in the API module Storages::ProjectStorages class BaseContract < ::ModelContract # "Concern" just injects a permission checking routine. diff --git a/modules/storages/app/controllers/storages/admin/oauth_clients_controller.rb b/modules/storages/app/controllers/storages/admin/oauth_clients_controller.rb index daacecbddc7..8384b25e66b 100644 --- a/modules/storages/app/controllers/storages/admin/oauth_clients_controller.rb +++ b/modules/storages/app/controllers/storages/admin/oauth_clients_controller.rb @@ -61,7 +61,7 @@ class Storages::Admin::OAuthClientsController < ApplicationController @oauth_client = service_result.result if service_result.success? flash[:notice] = I18n.t(:notice_successful_create) - if OpenProject::FeatureDecisions.automatically_managed_project_folders_active? && @storage.automatic_management_unspecified? + if @storage.automatic_management_unspecified? redirect_to new_admin_settings_storage_automatically_managed_project_folders_path(@storage) else redirect_to admin_settings_storage_path(@storage) diff --git a/modules/storages/app/controllers/storages/admin/projects_storages_controller.rb b/modules/storages/app/controllers/storages/admin/project_storages_controller.rb similarity index 93% rename from modules/storages/app/controllers/storages/admin/projects_storages_controller.rb rename to modules/storages/app/controllers/storages/admin/project_storages_controller.rb index 97f35a72c2b..cd88eb9ff8b 100644 --- a/modules/storages/app/controllers/storages/admin/projects_storages_controller.rb +++ b/modules/storages/app/controllers/storages/admin/project_storages_controller.rb @@ -33,7 +33,7 @@ # https://guides.rubyonrails.org/action_controller_overview.html # Called by: Calls to the controller methods are initiated by user Web GUI # actions and mapped to this controller by storages/config/routes.rb. -class Storages::Admin::ProjectsStoragesController < Projects::SettingsController +class Storages::Admin::ProjectStoragesController < Projects::SettingsController # This is the resource handled in this controller. # So the controller knows that the ID in params (URl) refer to instances of this model. # This defines @object as the model instance. @@ -46,13 +46,13 @@ class Storages::Admin::ProjectsStoragesController < Projects::SettingsController # This MenuController method defines the default menu item to be used (highlighted) # when rendering the main menu in the left (part of the base layout). # The menu item itself is registered in modules/storages/lib/open_project/storages/engine.rb - menu_item :settings_projects_storages + menu_item :settings_project_storages # Show a HTML page with the list of ProjectStorages # Called by: Project -> Settings -> File Storages def index # Just get the list of ProjectStorages associated with the project - @projects_storages = Storages::ProjectStorage.where(project: @project).includes(:storage) + @project_storages = Storages::ProjectStorage.where(project: @project).includes(:storage) # Render the list storages using ViewComponents in the /app/components folder which defines # the ways rows are rendered in a table layout. render '/storages/project_settings/index' @@ -86,7 +86,7 @@ class Storages::Admin::ProjectsStoragesController < Projects::SettingsController # Create success/error messages to the user if service_result.success? flash[:notice] = I18n.t(:notice_successful_create) - redirect_to project_settings_projects_storages_path + redirect_to project_settings_project_storages_path else @errors = service_result.errors @project_storage = service_result.result @@ -105,7 +105,7 @@ class Storages::Admin::ProjectsStoragesController < Projects::SettingsController @project_storage = @object @last_project_folders = Storages::LastProjectFolder - .where(projects_storage: @project_storage) + .where(project_storage: @project_storage) .pluck(:mode, :origin_folder_id) .to_h @@ -123,7 +123,7 @@ class Storages::Admin::ProjectsStoragesController < Projects::SettingsController if service_result.success? flash[:notice] = I18n.t(:notice_successful_update) - redirect_to project_settings_projects_storages_path + redirect_to project_settings_project_storages_path else @errors = service_result.errors @project_storage = @object @@ -143,7 +143,7 @@ class Storages::Admin::ProjectsStoragesController < Projects::SettingsController .on_failure { |service_result| flash[:error] = service_result.errors.full_messages } # Redirect the user to the URL of Projects -> Settings -> File Storages - redirect_to project_settings_projects_storages_path + redirect_to project_settings_project_storages_path end def destroy_info diff --git a/modules/storages/app/controllers/storages/admin/storages_controller.rb b/modules/storages/app/controllers/storages/admin/storages_controller.rb index facb3a52b12..e141e16588b 100644 --- a/modules/storages/app/controllers/storages/admin/storages_controller.rb +++ b/modules/storages/app/controllers/storages/admin/storages_controller.rb @@ -28,6 +28,8 @@ # Purpose: CRUD the global admin page of Storages (=Nextcloud servers) class Storages::Admin::StoragesController < ApplicationController + using Storages::Peripherals::ServiceResultRefinements + # See https://guides.rubyonrails.org/layouts_and_rendering.html for reference on layout layout 'admin' @@ -114,17 +116,17 @@ class Storages::Admin::StoragesController < ApplicationController end end - # Purpose: Destroy a specific Storage - # Called by: Global app/config/routes.rb to serve Web page def destroy Storages::Storages::DeleteService .new(user: User.current, model: @object) .call + .match( + # rubocop:disable Rails/ActionControllerFlashBeforeRender + on_success: ->(*) { flash[:notice] = I18n.t(:notice_successful_delete) }, + on_failure: ->(error) { flash[:error] = error.full_messages } + # rubocop:enable Rails/ActionControllerFlashBeforeRender + ) - # Displays a message box on the next page - flash[:notice] = I18n.t(:notice_successful_delete) - - # Redirect to the index page redirect_to admin_settings_storages_path end diff --git a/modules/storages/app/models/queries/storages/work_packages/filter/storage_id_filter.rb b/modules/storages/app/models/queries/storages/work_packages/filter/storage_id_filter.rb index 585a53a0de3..10c62921047 100644 --- a/modules/storages/app/models/queries/storages/work_packages/filter/storage_id_filter.rb +++ b/modules/storages/app/models/queries/storages/work_packages/filter/storage_id_filter.rb @@ -43,7 +43,7 @@ module Queries::Storages::WorkPackages::Filter end def joins - [:file_links, { project: :projects_storages }] + [:file_links, { project: :project_storages }] end def additional_where_condition diff --git a/modules/storages/app/models/queries/storages/work_packages/filter/storage_url_filter.rb b/modules/storages/app/models/queries/storages/work_packages/filter/storage_url_filter.rb index 8b8d3a13fc4..01479855b04 100644 --- a/modules/storages/app/models/queries/storages/work_packages/filter/storage_url_filter.rb +++ b/modules/storages/app/models/queries/storages/work_packages/filter/storage_url_filter.rb @@ -47,7 +47,7 @@ module Queries::Storages::WorkPackages::Filter end def joins - [{ file_links: :storage }, { project: :projects_storages }] + [{ file_links: :storage }, { project: :project_storages }] end def additional_where_condition diff --git a/modules/storages/app/models/storages/last_project_folder.rb b/modules/storages/app/models/storages/last_project_folder.rb index 9efcd7488d0..701bf376c1c 100644 --- a/modules/storages/app/models/storages/last_project_folder.rb +++ b/modules/storages/app/models/storages/last_project_folder.rb @@ -27,7 +27,7 @@ #++ class Storages::LastProjectFolder < ApplicationRecord - belongs_to :projects_storage, class_name: 'Storages::ProjectStorage' + belongs_to :project_storage, class_name: 'Storages::ProjectStorage' enum mode: { inactive: 'inactive', manual: 'manual', automatic: 'automatic' }.freeze end diff --git a/modules/storages/app/models/storages/project_storage.rb b/modules/storages/app/models/storages/project_storage.rb index 20cd9c9b41f..36cd29fc573 100644 --- a/modules/storages/app/models/storages/project_storage.rb +++ b/modules/storages/app/models/storages/project_storage.rb @@ -31,10 +31,6 @@ # WorkPackages in the project. # See also: file_link.rb and storage.rb class Storages::ProjectStorage < ApplicationRecord - # set table name explicitly (would be guessed from model class name and be - # project_storages otherwise) - self.table_name = 'projects_storages' - # ProjectStorage sits between Project and Storage. belongs_to :project, touch: true belongs_to :storage, touch: true, class_name: 'Storages::Storage' @@ -42,8 +38,7 @@ class Storages::ProjectStorage < ApplicationRecord has_many :last_project_folders, class_name: 'Storages::LastProjectFolder', - dependent: :destroy, - foreign_key: :projects_storage_id + dependent: :destroy # There should be only one ProjectStorage per project and storage. validates :project, uniqueness: { scope: :storage } @@ -59,4 +54,20 @@ class Storages::ProjectStorage < ApplicationRecord def project_folder_path "#{storage.group_folder}/#{project.name.gsub('/', '|')} (#{project.id})/" end + + def project_folder_path_escaped + escape_path(project_folder_path) + end + + def file_inside_project_folder?(escaped_file_path) + escaped_file_path.match?(%r|^/#{project_folder_path_escaped}|) + end + + private + + def escape_path(path) + ::Storages::Peripherals::StorageInteraction::Nextcloud::Util.escape_path( + path + ) + end end diff --git a/modules/storages/app/models/storages/storage.rb b/modules/storages/app/models/storages/storage.rb index d35d106dd0a..4684b28d7b8 100644 --- a/modules/storages/app/models/storages/storage.rb +++ b/modules/storages/app/models/storages/storage.rb @@ -50,9 +50,9 @@ class Storages::Storage < ApplicationRecord # Basically every OpenProject object has a creator belongs_to :creator, class_name: 'User' # A project manager can enable/disable Storages per project. - has_many :projects_storages, dependent: :destroy, class_name: 'Storages::ProjectStorage' + has_many :project_storages, dependent: :destroy, class_name: 'Storages::ProjectStorage' # We can get the list of projects with this Storage enabled. - has_many :projects, through: :projects_storages + has_many :projects, through: :project_storages # The OAuth client credentials that OpenProject will use to obtain user specific # access tokens from the storage server, i.e a Nextcloud serer. has_one :oauth_client, as: :integration, dependent: :destroy @@ -73,7 +73,7 @@ class Storages::Storage < ApplicationRecord all else where( - projects_storages: ::Storages::ProjectStorage.where( + project_storages: ::Storages::ProjectStorage.where( project: Project.allowed_to(user, :view_file_links) ) ) @@ -81,7 +81,7 @@ class Storages::Storage < ApplicationRecord end scope :not_enabled_for_project, ->(project) do - where.not(id: project.projects_storages.pluck(:storage_id)) + where.not(id: project.project_storages.pluck(:storage_id)) end def self.shorten_provider_type(provider_type) diff --git a/modules/storages/app/models/storages/storage_file.rb b/modules/storages/app/models/storages/storage_file.rb index c2d3edde008..59dc8fed6ce 100644 --- a/modules/storages/app/models/storages/storage_file.rb +++ b/modules/storages/app/models/storages/storage_file.rb @@ -26,21 +26,43 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Storages::StorageFile - attr_reader :id, :name, :size, :mime_type, :created_at, :last_modified_at, :created_by_name, :last_modified_by_name, - :location, :permissions - - def initialize(id, name, size, mime_type, created_at, last_modified_at, created_by_name, last_modified_by_name, - location, permissions) - @id = id - @name = name - @size = size - @mime_type = mime_type - @created_at = created_at - @last_modified_at = last_modified_at - @created_by_name = created_by_name - @last_modified_by_name = last_modified_by_name - @location = location - @permissions = permissions +module Storages + StorageFile = Data.define( + :id, + :name, + :size, + :mime_type, + :created_at, + :last_modified_at, + :created_by_name, + :last_modified_by_name, + :location, + :permissions + ) do + def initialize( + id:, + name:, + size: nil, + mime_type: nil, + created_at: nil, + last_modified_at: nil, + created_by_name: nil, + last_modified_by_name: nil, + location: nil, + permissions: nil + ) + super( + id:, + name:, + size:, + mime_type:, + created_at:, + last_modified_at:, + created_by_name:, + last_modified_by_name:, + location:, + permissions: + ) + end end end diff --git a/modules/storages/app/models/storages/storage_file_info.rb b/modules/storages/app/models/storages/storage_file_info.rb new file mode 100644 index 00000000000..4bece7e87b9 --- /dev/null +++ b/modules/storages/app/models/storages/storage_file_info.rb @@ -0,0 +1,107 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Storages + StorageFileInfo = Data.define( + :status, + :status_code, + :id, + :name, + :last_modified_at, + :created_at, + :mime_type, + :size, + :owner_name, + :owner_id, + :trashed, + :last_modified_by_name, + :last_modified_by_id, + :permissions, + :location + ) do + def initialize( + status:, + status_code:, + id:, + name: nil, + last_modified_at: nil, + created_at: nil, + mime_type: nil, + size: nil, + owner_name: nil, + owner_id: nil, + trashed: nil, + last_modified_by_name: nil, + last_modified_by_id: nil, + permissions: nil, + location: nil + ) + super( + status:, + status_code:, + id:, + name:, + last_modified_at:, + created_at:, + mime_type:, + size:, + owner_name:, + owner_id:, + trashed:, + last_modified_by_name:, + last_modified_by_id:, + permissions:, + location: + ) + end + end +end + +# class Storages::StorageFileInfo +# attr_reader :status, :status_code, :id, :name, :last_modified_at, :created_at, :mime_type, :size, :owner_name, +# :owner_id, :trashed, :last_modified_by_name, :last_modified_by_id, :permissions, :location +# +# def initialize(status, status_code, id, name, last_modified_at, created_at, mime_type, size, owner_name, owner_id, +# trashed, last_modified_by_name, last_modified_by_id, permissions, location) +# @status = status +# @status_code = status_code +# @id = id +# @name = name +# @last_modified_at = last_modified_at +# @created_at = created_at +# @mime_type = mime_type +# @size = size +# @owner_name = owner_name +# @owner_id = owner_id +# @trashed = trashed +# @last_modified_by_name = last_modified_by_name +# @last_modified_by_id = last_modified_by_id +# @permissions = permissions +# @location = location +# end +# end diff --git a/modules/storages/app/services/projects/copy/file_links_dependent_service.rb b/modules/storages/app/services/projects/copy/file_links_dependent_service.rb index 59ece00279d..a7521e941f8 100644 --- a/modules/storages/app/services/projects/copy/file_links_dependent_service.rb +++ b/modules/storages/app/services/projects/copy/file_links_dependent_service.rb @@ -38,32 +38,92 @@ module Projects::Copy protected + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/PerceivedComplexity def copy_dependency(*) - # If no work packages were copied, we cannot copy their attachments + # If no work packages were copied, we cannot copy their file_links return unless state.work_package_id_lookup - state.work_package_id_lookup.each do |old_wp_id, new_wp_id| - create_work_package_file_links(old_wp_id, new_wp_id) + source_wp_ids = state.work_package_id_lookup.keys + Storages::FileLink + .where(container_id: source_wp_ids, container_type: "WorkPackage") + .group_by(&:storage_id) + .filter { |_storage_id, source_file_links| source_file_links.any? } + .each do |(storage_id, source_file_links)| + tmp = state + .copied_project_storages + .find { |item| item["source"].storage_id == storage_id } + source_project_storage = tmp['source'] + target_project_storage = tmp['target'] + storage_requests = Storages::Peripherals::StorageRequests.new(storage: source_project_storage.storage) + + if source_project_storage.project_folder_mode == 'automatic' + files_info_query_result = files_info_query(storage_requests:, + file_ids: source_file_links.map(&:origin_id)) + folder_files_file_ids_deep_query_result = folder_files_file_ids_deep_query( + storage_requests:, + path: target_project_storage.project_folder_path + ) + source_file_links.each do |old_file_link| + attributes = { + storage_id: old_file_link.storage_id, + creator_id: User.current.id, + container_id: state.work_package_id_lookup[old_file_link.container_id.to_s], + container_type: 'WorkPackage', + origin_name: old_file_link.origin_name, + origin_mime_type: old_file_link.origin_mime_type + } + + original_file_location = files_info_query_result + .find { |i| i.id.to_i == old_file_link.origin_id.to_i } + .location + + attributes['origin_id'] = + if source_project_storage.file_inside_project_folder?(original_file_location) + new_file_location = original_file_location.gsub( + source_project_storage.project_folder_path_escaped, + target_project_storage.project_folder_path_escaped + ) + new_file_location = CGI.unescape(new_file_location[1..]) + folder_files_file_ids_deep_query_result[new_file_location]['fileid'] + else + old_file_link.origin_id + end + Storages::FileLinks::CreateService.new(user: User.current).call(attributes) + end + else + source_file_links.each do |old_file_link| + attributes = { + storage_id: old_file_link.storage_id, + creator_id: User.current.id, + container_id: state.work_package_id_lookup[old_file_link.container_id.to_s], + container_type: 'WorkPackage', + origin_name: old_file_link.origin_name, + origin_mime_type: old_file_link.origin_mime_type, + origin_id: old_file_link.origin_id + } + Storages::FileLinks::CreateService.new(user: User.current).call(attributes) + end + end end end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/PerceivedComplexity - def create_work_package_file_links(old_wp_id, new_wp_id) - Storages::FileLink.where(container_id: old_wp_id).each do |file_link| - create_file_link(file_link, new_wp_id) - end + def files_info_query(storage_requests:, file_ids:) + storage_requests + .files_info_query + .call(user: User.current, file_ids:) + .on_failure { |r| add_error!("files_info_query", r.to_active_model_errors) } + .result end - def create_file_link(file_link, new_wp_id) - attributes = file_link - .attributes.dup.except('id', 'container_id', 'created_at', 'updated_at') - .merge('container_id' => new_wp_id) - - service_result = Storages::FileLinks::CreateService - .new(user: User.current) - .call(attributes) - - copied_file_link = service_result.result - copied_file_link.save + def folder_files_file_ids_deep_query(storage_requests:, path:) + storage_requests + .folder_files_file_ids_deep_query + .call(path:) + .on_failure { |r| add_error!("folder_files_file_ids_deep_query", r.to_active_model_errors) } + .result end end end diff --git a/modules/storages/app/services/projects/copy/storage_project_folders_dependent_service.rb b/modules/storages/app/services/projects/copy/storage_project_folders_dependent_service.rb index d18fbee94c0..a75fb12df3f 100644 --- a/modules/storages/app/services/projects/copy/storage_project_folders_dependent_service.rb +++ b/modules/storages/app/services/projects/copy/storage_project_folders_dependent_service.rb @@ -45,10 +45,16 @@ module Projects::Copy return unless state.copied_project_storages state.copied_project_storages.each do |copied_project_storage| - if copied_project_storage[:source].project_folder_automatic? - copy_project_folder(copied_project_storage[:source], copied_project_storage[:target]) - - update_project_folder_id(copied_project_storage[:target]) + source = copied_project_storage[:source] + target = copied_project_storage[:target] + if source.project_folder_automatic? + copy_project_folder(source, target) + update_project_folder_id(target) + elsif source.project_folder_manual? + target.update!( + project_folder_id: source.project_folder_id, + project_folder_mode: 'manual' + ) end end end @@ -69,22 +75,18 @@ module Projects::Copy def update_project_folder_id(project_storage) destination_folder_name = project_storage.project_folder_path - query_params = { - depth: '0', - path: destination_folder_name, - props: %w[oc:fileid] - } - Storages::Peripherals::StorageRequests .new(storage: project_storage.storage) - .propfind_query - .call(**query_params) + .file_ids_query + .call(path: destination_folder_name) .match( - on_success: ->(r) do - file_id = r[destination_folder_name]["fileid"] - project_storage.update!(project_folder_id: file_id) + on_success: ->(file_ids) do + project_storage.update!( + project_folder_id: file_ids.values.first['fileid'], + project_folder_mode: 'automatic' + ) end, - on_failure: ->(r) { add_error!(destination_folder_name, r.to_active_model_errors) } + on_failure: ->(error) { add_error!(destination_folder_name, error.to_active_model_errors) } ) end end diff --git a/modules/storages/app/services/projects/copy/storages_dependent_service.rb b/modules/storages/app/services/projects/copy/storages_dependent_service.rb index d1b33513a29..85fadcf137a 100644 --- a/modules/storages/app/services/projects/copy/storages_dependent_service.rb +++ b/modules/storages/app/services/projects/copy/storages_dependent_service.rb @@ -40,30 +40,21 @@ module Projects::Copy protected + # rubocop:disable Metrics/AbcSize def copy_dependency(*) - copied_project_storages = [] + state.copied_project_storages = source.project_storages.each_with_object([]) do |source_project_storage, array| + project_storage_copy = + ::Storages::ProjectStorages::CreateService + .new(user: User.current) + .call(storage_id: source_project_storage.storage_id, + project_id: target.id, + project_folder_mode: 'inactive') + .on_failure { |r| add_error!(source_project_storage.class.to_s, r.to_active_model_errors) } + .result - source.projects_storages.find_each do |project_storage| - copied_project_storages << { source: project_storage, target: create_project_storage(project_storage) } + array << { source: source_project_storage, target: project_storage_copy } end - - state.copied_project_storages = copied_project_storages - end - - private - - def create_project_storage(project_storage) - attributes = project_storage - .attributes.dup.except('id', 'project_id', 'created_at', 'updated_at') - .merge('project_id' => target.id) - - service_result = ::Storages::ProjectStorages::CreateService - .new(user: User.current) - .call(attributes) - - copied_storage = service_result.result - copied_storage.save - copied_storage end + # rubocop:enable Metrics/AbcSize end end diff --git a/modules/storages/app/services/storages/file_link_sync_service.rb b/modules/storages/app/services/storages/file_link_sync_service.rb index 674253ee851..8c484fb84d2 100644 --- a/modules/storages/app/services/storages/file_link_sync_service.rb +++ b/modules/storages/app/services/storages/file_link_sync_service.rb @@ -26,205 +26,89 @@ # See COPYRIGHT and LICENSE files for more details. #++ -# Handle synchronization of FileLinks with external data store such as Storages class Storages::FileLinkSyncService - FILESINFO_URL_PATH = "/ocs/v1.php/apps/integration_openproject/filesinfo".freeze + using Storages::Peripherals::ServiceResultRefinements def initialize(user:) @user = user - @service_result = ServiceResult.success(result: []) end def call(file_links) - @file_links = file_links - storage_file_link_hash = @file_links.group_by(&:storage_id) - storage_file_link_hash.each do |storage_id, storage_file_links| - # Add new types of storages here. We currently only support Nextcloud - sync_nextcloud(storage_id, storage_file_links) - end + resulting_file_links = file_links + .group_by(&:storage_id) + .map { |storage_id, storage_file_links| sync_nextcloud(storage_id, storage_file_links) } + .reduce([]) do |state, sync_result| + sync_result.match( + on_success: ->(sr) { state + sr }, + on_failure: ->(_) { state } + ) + end - @service_result + ServiceResult.success(result: resulting_file_links) end private - # Get the OAuthClientToken that will authenticate us against Nextcloud - def get_connection_manager(storage_id) - oauth_client = OAuthClient.find_by(integration_id: storage_id) - ::OAuthClients::ConnectionManager.new(user: @user, oauth_client:) - end - - def get_oauth_client_token(connection_manager) - access_token_service_result = connection_manager.get_access_token # No scope for Nextcloud... - if access_token_service_result.failure? - return nil - end - - access_token_service_result.result - end - - # Sync a Nextcloud storage def sync_nextcloud(storage_id, file_links) - connection_manager = get_connection_manager(storage_id) - oauth_client_token = get_oauth_client_token(connection_manager) - if oauth_client_token.nil? - set_error_for_file_links(file_links) - return - end - - nextcloud_request_result = connection_manager.request_with_token_refresh(oauth_client_token) do |token| - response = request_files_info(token, file_links.map(&:origin_id)) - Rails.logger.debug { "Got nextcloud filesinfo response: #{response.inspect}" } - # Parse HTTP response an return a ServiceResult with: - # success: result= Hash with Nextcloud filesinfo (name of endpoint) data - # failure: result= :error or :not_authorized - parse_files_info_response(response) - end - # Pass errors from Nextcloud into service result - @service_result.merge!(nextcloud_request_result) - - if nextcloud_request_result.failure? - set_error_for_file_links(file_links) - return - end - - set_file_link_permissions(file_links, nextcloud_request_result.result) + ::Storages::Peripherals::StorageRequests + .new(storage: ::Storages::Storage.find(storage_id)) + .files_info_query + .call(user: @user, file_ids: file_links.map(&:origin_id)) + .map { |file_infos| to_hash(file_infos) } + .match( + on_success: set_file_link_permissions(file_links), + on_failure: ->(_) { + ServiceResult.success(result: file_links.map do |file_link| + file_link.origin_permission = :error + file_link + end) + } + ) end - def set_file_link_permissions(file_links, parsed_response) - file_links.each do |file_link| - origin_file_info_hash = parsed_response[file_link.origin_id] + def to_hash(file_infos) + file_infos.index_by { |file_info| file_info.id.to_s }.to_h + end - case origin_file_info_hash['statuscode'] - when 200 # Successfully found - update infos - next if origin_file_info_hash['trashed'] # moved to trash in Nextcloud + def set_file_link_permissions(file_links) + ->(file_infos) do + resulting_file_links = [] - sync_single_file(file_link, origin_file_info_hash) - file_link.origin_permission = :view - when 403 # Forbidden - file from other user - file_link.origin_permission = :not_allowed - when 404 # Not found - permanently deleted in Nextcloud - file_link.destroy - next # don't save and don't add file_link to result! - else - file_link.origin_permission = :error + file_links.each do |file_link| + file_info = file_infos[file_link.origin_id] + + case file_info.status_code + when 200 + next if file_info.trashed + + update_file_link(file_link, file_info) + + file_link.origin_permission = :view + when 403 + file_link.origin_permission = :not_allowed + when 404 + file_link.destroy + next + else + file_link.origin_permission = :error + end + + resulting_file_links << file_link + file_link.save end - @service_result.result << file_link - file_link.save # Only saves to database if some field has actually changed. + ServiceResult.success(result: resulting_file_links) end end - # Write the updated information from Nextcloud to a single file - # @param storage Storage of the file - # @param origin_file_id Nextcloud ID of the file - # @param origin_file_info_hash Hash with updated information from Nextcloud - # "24" => { - # "id" : 24, # origin_file_id - # "ctime" : 0, # Linux epoch file creation +overwrite - # "mtime" : 1655301278, # Linux epoch file modification +overwrite - # "mimetype" : "application/pdf", # +overwrite - # "name" : "Nextcloud Manual.pdf", # "Canonical" name, could changed by owner +overwrite - # "owner_id" : "admin", # ID at Nextcloud side +overwrite - # "owner_name" : "admin", # Name at Nextcloud side +overwrite - # "size" : 12706214, # Not used yet in OpenProject +overwrite - # "status" : "OK", # Not used yet - # "statuscode" : 200, # Not used yet - # "trashed" : false # Exclude trashed files from result array of FileLinks - # } - # In case of permission errors (depending on the current user) we get: - # "24" => { "status" => "Forbidden", "statuscode" => 403 } - # In case of completely deleted file or other errors we might also get: - # "24" = { "status" => "Not Found", "statuscode" => 404 } - # rubocop:disable Metrics/AbcSize - def sync_single_file(file_link, origin_file_info_hash) - file_link.origin_mime_type = origin_file_info_hash["mimetype"] - file_link.origin_created_by_name = origin_file_info_hash["owner_name"] - file_link.origin_last_modified_by_name = origin_file_info_hash["modifier_name"] - file_link.origin_name = origin_file_info_hash["name"] - file_link.origin_created_at = Time.zone.at(origin_file_info_hash["ctime"]) - file_link.origin_updated_at = Time.zone.at(origin_file_info_hash["mtime"]) + def update_file_link(file_link, file_info) + file_link.origin_mime_type = file_info.mime_type + file_link.origin_created_by_name = file_info.owner_name + file_link.origin_last_modified_by_name = file_info.last_modified_by_name + file_link.origin_name = file_info.name + file_link.origin_created_at = file_info.created_at + file_link.origin_updated_at = file_info.last_modified_at file_link end - - # rubocop:enable Metrics/AbcSize - - def set_error_for_file_links(storage_file_links) - @service_result.result += storage_file_links.each { |file_link| file_link.origin_permission = :error } - @service_result.success = false - end - - # Check the Nextcloud status of a list of files: - # The endpoint returns "canonical" infos per file. Canonical means that the attributes are the same - # as the owner sees them and not the current user. If it was the current user the file name could be - # different for that user as a shared file can be renamed. - # @param file_ids An array of Nextcloud IDs of files to check - # @return A HTTP response or an Exception object - # - # curl -H "Accept: application/json" -H "Content-Type:application/json" -H "OCS-APIRequest: true" - # -u USER:PASSWD http://my.nc.org/ocs/v1.php/apps/integration_openproject/filesinfo - # -X POST -d '{"fileIds":[FILE_ID_1,FILE_ID_2,...]}' - def request_files_info(token, file_ids) - host = token.oauth_client.integration.host - uri = URI.parse(File.join(host, FILESINFO_URL_PATH)) - request = build_files_info_request(uri, token, file_ids) - opts = request_file_info_options.merge({ use_ssl: uri.scheme == 'https' }) - - begin - Net::HTTP.start(uri.hostname, uri.port, opts) do |http| - http.request(request) - end - rescue StandardError => e - e - end - end - - # HTTP Request options: Keep the request short for the sake of the front-end - def request_file_info_options - { - max_retries: 0, - open_timeout: 5, # seconds - read_timeout: 3 # seconds - } - end - - def build_files_info_request(uri, token, file_ids) - request = Net::HTTP::Post.new(uri) - request.body = { fileIds: file_ids }.to_json - { - 'Content-Type': 'application/json', - 'OCS-APIRequest': 'true', - Accept: 'application/json', - Authorization: "Bearer #{token.access_token}" - }.each { |header, value| request[header] = value } - - request - end - - # Takes a response from querying Nextcloud file IDS (an Exception or a HTTP::Response), - # parses the returned JSON and - # @returns ServiceResult containing data in result and success=true, - # or success=false with result=:error or result=:not_authorized. - def parse_files_info_response(response) - return ServiceResult.failure(result: :error) if files_info_response_error?(response) - return ServiceResult.failure(result: :not_authorized) if ["401", "403"].include?(response.code) - return ServiceResult.failure(result: :error) unless response.code == "200" - - begin - response_hash = JSON.parse(response.body).dig('ocs', 'data') - rescue JSON::ParserError => e - return ServiceResult.failure(result: :error) - .errors.add(:base, "JSON parser error: #{e.message}") - end - - ServiceResult.success(result: response_hash) - end - - def files_info_response_error?(response) - response.nil? || - response.is_a?(StandardError) || - !response.key?('content-type') || # Reply without content-type can't be valid. - response['content-type'].split(';').first.strip.downcase != 'application/json' - end end diff --git a/modules/storages/app/services/storages/group_folder_properties_sync_service.rb b/modules/storages/app/services/storages/group_folder_properties_sync_service.rb index 17d291681ce..70a8d5f8ea3 100644 --- a/modules/storages/app/services/storages/group_folder_properties_sync_service.rb +++ b/modules/storages/app/services/storages/group_folder_properties_sync_service.rb @@ -58,21 +58,22 @@ class Storages::GroupFolderPropertiesSyncService set_group_folder_root_permissions - @storage.projects_storages - .automatic - .includes(project: %i[users enabled_modules]) - .each do |project_storage| - project = project_storage.project - project_folder_path = project_storage.project_folder_path - @project_folder_ids_used_in_openproject << ensure_project_folder(project_storage:, project_folder_path:) + @storage.project_storages + .automatic + .includes(project: %i[users enabled_modules]) + .each do |project_storage| + project = project_storage.project + project_folder_path = project_storage.project_folder_path + @project_folder_ids_used_in_openproject << ensure_project_folder(project_storage:, project_folder_path:) - set_project_folder_permissions(path: project_folder_path, project:) - end + set_project_folder_permissions(path: project_folder_path, project:) + end hide_inactive_project_folders add_active_users_to_group remove_inactive_users_from_group end + # rubocop:enable Metrics/AbcSize private @@ -106,6 +107,7 @@ class Storages::GroupFolderPropertiesSyncService # then the value inside the local variable will not be updated project_storage.project_folder_id end + # rubocop:enable Metrics/AbcSize def file_ids @@ -114,18 +116,11 @@ class Storages::GroupFolderPropertiesSyncService def folders_properties @folders_properties ||= - begin - query_params = { - depth: '1', - path: @group_folder, - props: %w[oc:fileid] - } - @requests - .propfind_query - .call(**query_params) - .on_failure(&failure_handler('propfind_query', query_params)) - .result - end + @requests + .file_ids_query + .call(path: @group_folder) + .on_failure(&failure_handler('file_ids_query', { path: @group_folder })) + .result end def rename_folder(source:, target:) @@ -137,10 +132,10 @@ class Storages::GroupFolderPropertiesSyncService def create_folder(path:, project_storage:) @requests.create_folder_command.call(folder_path: path) - .match( - on_success: ->(_) { ServiceResult.success(result: [project_storage, path]) }, - on_failure: failure_handler('create_folder_command', { folder_path: path }) - ) + .match( + on_success: ->(_) { ServiceResult.success(result: [project_storage, path]) }, + on_failure: failure_handler('create_folder_command', { folder_path: path }) + ) end def save_file_id @@ -151,17 +146,12 @@ class Storages::GroupFolderPropertiesSyncService def obtain_file_id ->((project_storage, path)) do - query_params = { - depth: '0', - path:, - props: %w[oc:fileid] - } @requests - .propfind_query - .call(**query_params) + .file_ids_query + .call(path:) .match( - on_success: ->(result) { ServiceResult.success(result: [project_storage, result.dig(path, 'fileid')]) }, - on_failure: failure_handler('propfind_query', query_params) + on_success: ->(file_ids) { ServiceResult.success(result: [project_storage, file_ids.dig(path, 'fileid')]) }, + on_failure: failure_handler('file_id_query', { path: }) ) end end @@ -197,10 +187,10 @@ class Storages::GroupFolderPropertiesSyncService @group_users ||= begin query_params = { group: @group } @requests - .group_users_query - .call(**query_params) - .on_failure(&failure_handler('group_users_query', query_params)) - .result + .group_users_query + .call(**query_params) + .on_failure(&failure_handler('group_users_query', query_params)) + .result end end diff --git a/modules/storages/app/services/storages/project_storages/create_service.rb b/modules/storages/app/services/storages/project_storages/create_service.rb index 20fac5fc116..b9ce5fffa57 100644 --- a/modules/storages/app/services/storages/project_storages/create_service.rb +++ b/modules/storages/app/services/storages/project_storages/create_service.rb @@ -35,8 +35,12 @@ module Storages::ProjectStorages super(service_call) project_storage = service_call.result - add_historical_data(service_call) if project_storage.project_folder_mode.to_sym != :inactive - Helper.trigger_nextcloud_synchronization(project_storage.project_folder_mode) + project_folder_mode = project_storage.project_folder_mode.to_sym + add_historical_data(service_call) if project_folder_mode != :inactive + OpenProject::Notifications.send( + OpenProject::Events::PROJECT_STORAGE_CREATED, + project_folder_mode: + ) service_call end @@ -48,7 +52,7 @@ module Storages::ProjectStorages last_project_folder_result = Helper.create_last_project_folder( user:, - projects_storage_id: project_storage.id, + project_storage_id: project_storage.id, origin_folder_id: project_storage.project_folder_id, mode: project_storage.project_folder_mode ) diff --git a/modules/storages/app/services/storages/project_storages/delete_service.rb b/modules/storages/app/services/storages/project_storages/delete_service.rb index 1fa93ccc212..b0d64fc933f 100644 --- a/modules/storages/app/services/storages/project_storages/delete_service.rb +++ b/modules/storages/app/services/storages/project_storages/delete_service.rb @@ -30,31 +30,12 @@ module Storages::ProjectStorages # Performs the deletion in the superclass. Associated FileLinks are deleted # by the model before_destroy hook. class DeleteService < ::BaseServices::Delete - using Storages::Peripherals::ServiceResultRefinements + def before_perform(*) + delete_project_folder if model.storage.is_a?(Storages::NextcloudStorage) - # rubocop:disable Metrics/AbcSize - def before_perform(params, service_result) - before_result = super(params, service_result) - return before_result if before_result.failure? || !model.storage.is_a?(Storages::NextcloudStorage) - - Storages::Peripherals::StorageRequests - .new(storage: model.storage) - .delete_folder_command - .call(location: model.project_folder_path) - .match( - on_success: ->(*) { ServiceResult.success(result: model) }, - on_failure: ->(error) do - if error.code == :not_found - ServiceResult.success(result: model) - else - ServiceResult.failure(errors: error.to_active_model_errors) - end - end - ) + super end - # rubocop:enable Metrics/AbcSize - # "persist" is a callback from BaseContracted.perform # that is supposed to do the actual work in a contract. # So in a DeleteService it performs the actual delete, @@ -63,13 +44,25 @@ module Storages::ProjectStorages def persist(service_result) # Perform the @object.destroy etc. in the super-class super(service_result).tap do |deletion_result| - delete_associated_file_links if deletion_result.success? - Helper.trigger_nextcloud_synchronization(model.project_folder_mode) + if deletion_result.success? + delete_associated_file_links + OpenProject::Notifications.send( + OpenProject::Events::PROJECT_STORAGE_DESTROYED, + project_folder_mode: deletion_result.result.project_folder_mode.to_sym + ) + end end end private + def delete_project_folder + Storages::Peripherals::StorageRequests + .new(storage: model.storage) + .delete_folder_command + .call(location: model.project_folder_path) + end + # Delete FileLinks with the same Storage as the ProjectStorage. # Also, they are attached to WorkPackages via the Project. def delete_associated_file_links diff --git a/modules/storages/app/services/storages/project_storages/helper.rb b/modules/storages/app/services/storages/project_storages/helper.rb index 5a7db62c2de..38072c205f4 100644 --- a/modules/storages/app/services/storages/project_storages/helper.rb +++ b/modules/storages/app/services/storages/project_storages/helper.rb @@ -29,10 +29,10 @@ module Storages::ProjectStorages::Helper module_function - def create_last_project_folder(user:, projects_storage_id:, origin_folder_id:, mode:) + def create_last_project_folder(user:, project_storage_id:, origin_folder_id:, mode:) ::Storages::LastProjectFolders::CreateService .new(user:) - .call(projects_storage_id:, origin_folder_id:, mode: mode.to_sym) + .call(project_storage_id:, origin_folder_id:, mode: mode.to_sym) end def update_last_project_folder(user:, project_folder:, origin_folder_id:) @@ -40,8 +40,4 @@ module Storages::ProjectStorages::Helper .new(model: project_folder, user:) .call(origin_folder_id:) end - - def trigger_nextcloud_synchronization(project_folder_mode) - Storages::ManageNextcloudIntegrationEventsJob.perform_later if project_folder_mode.to_sym == :automatic - end end diff --git a/modules/storages/app/services/storages/project_storages/update_service.rb b/modules/storages/app/services/storages/project_storages/update_service.rb index 202f67e7fec..908ef2f616d 100644 --- a/modules/storages/app/services/storages/project_storages/update_service.rb +++ b/modules/storages/app/services/storages/project_storages/update_service.rb @@ -36,8 +36,12 @@ module Storages::ProjectStorages super(service_call) project_storage = service_call.result - add_historical_data(service_call) if project_storage.project_folder_mode.to_sym != :inactive - Helper.trigger_nextcloud_synchronization(model.project_folder_mode) + project_folder_mode = project_storage.project_folder_mode.to_sym + add_historical_data(service_call) if project_folder_mode != :inactive + OpenProject::Notifications.send( + OpenProject::Events::PROJECT_STORAGE_UPDATED, + project_folder_mode: + ) service_call end @@ -48,7 +52,7 @@ module Storages::ProjectStorages project_storage = service_call.result project_folder = ::Storages::LastProjectFolder .find_by( - projects_storage_id: project_storage.id, + project_storage_id: project_storage.id, mode: project_storage.project_folder_mode ) @@ -56,7 +60,7 @@ module Storages::ProjectStorages if project_folder.nil? Helper.create_last_project_folder( user:, - projects_storage_id: project_storage.id, + project_storage_id: project_storage.id, origin_folder_id: project_storage.project_folder_id, mode: project_storage.project_folder_mode ) diff --git a/modules/storages/app/services/storages/storages/delete_service.rb b/modules/storages/app/services/storages/storages/delete_service.rb index abe2b98f2fb..e0b4ba3d69d 100644 --- a/modules/storages/app/services/storages/storages/delete_service.rb +++ b/modules/storages/app/services/storages/storages/delete_service.rb @@ -29,5 +29,18 @@ # See also: create_service.rb for comments module Storages::Storages class DeleteService < ::BaseServices::Delete + def before_perform(*) + delete_project_storages + + super + end + + private + + def delete_project_storages + model.project_storages.map do |project_storage| + Storages::ProjectStorages::DeleteService.new(user:, model: project_storage).call + end + end end end diff --git a/modules/storages/app/views/storages/admin/storages/edit.html.erb b/modules/storages/app/views/storages/admin/storages/edit.html.erb index f9c23388fc3..19504c80415 100644 --- a/modules/storages/app/views/storages/admin/storages/edit.html.erb +++ b/modules/storages/app/views/storages/admin/storages/edit.html.erb @@ -47,28 +47,28 @@ <% end %> <% end %> -<% if OpenProject::FeatureDecisions.automatically_managed_project_folders_active? %> - <%= render(AttributeGroups::AttributeGroupComponent.new) do |component| %> - <% component.with_header(title: t("storages.page_titles.managed_project_folders.title")) %> - <% if @object.automatic_management_unspecified? %> - <%= link_to(t("storages.buttons.configure"), new_admin_settings_storage_automatically_managed_project_folders_path(@object), class: 'button -with-icon icon-add') %> - <% else %> +<%= render(AttributeGroups::AttributeGroupComponent.new) do |component| %> + <% component.with_header(title: t("storages.page_titles.managed_project_folders.title")) %> + + <% if @object.automatic_management_unspecified? %> + <%= link_to(t("storages.buttons.configure"), new_admin_settings_storage_automatically_managed_project_folders_path(@object), class: 'button -with-icon icon-add') %> + <% else %> + <% component.with_attribute( + key: t(:"storages.label_managed_project_folders.automatically_managed_folders"), + value: @object.automatically_managed? ? t(:"storages.label_active") : t(:"storages.label_inactive") + ) %> + + <% if @object.automatically_managed? %> <% component.with_attribute( - key: t(:"storages.label_managed_project_folders.automatically_managed_folders"), - value: @object.automatically_managed? ? t(:"storages.label_active") : t(:"storages.label_inactive") + key: t(:"storages.label_managed_project_folders.application_password"), + value: "●●●●●●●●●●●●●●●●" ) %> - - <% if @object.automatically_managed? %> - <% component.with_attribute( - key: t(:"storages.label_managed_project_folders.application_password"), - value: "●●●●●●●●●●●●●●●●" - ) %> - <% end %> - - <%= link_to(t("storages.buttons.edit_automatically_managed_project_folders"), - edit_admin_settings_storage_automatically_managed_project_folders_path(@object), - class: 'button -with-icon icon-edit') %> <% end %> + + <%= link_to(t("storages.buttons.edit_automatically_managed_project_folders"), + edit_admin_settings_storage_automatically_managed_project_folders_path(@object), + class: 'button -with-icon icon-edit') %> <% end %> <% end %> + diff --git a/modules/storages/app/views/storages/admin/storages/show.html.erb b/modules/storages/app/views/storages/admin/storages/show.html.erb index 426af414e7c..2c060f17ed3 100644 --- a/modules/storages/app/views/storages/admin/storages/show.html.erb +++ b/modules/storages/app/views/storages/admin/storages/show.html.erb @@ -98,24 +98,24 @@ See COPYRIGHT and LICENSE files for more details. <% end %> <% end %> -<% if OpenProject::FeatureDecisions.automatically_managed_project_folders_active? %> - <%= render(AttributeGroups::AttributeGroupComponent.new) do |component| %> - <% component.with_header(title: t("storages.page_titles.managed_project_folders.title")) %> - <% if @object.automatic_management_unspecified? %> - <%= t("storages.automatically_managed_project_folder_missing") %> - <% else %> +<%= render(AttributeGroups::AttributeGroupComponent.new) do |component| %> + <% component.with_header(title: t("storages.page_titles.managed_project_folders.title")) %> + + <% if @object.automatic_management_unspecified? %> + <%= t("storages.automatically_managed_project_folder_missing") %> + <% else %> + <% component.with_attribute( + key: t(:"storages.label_managed_project_folders.automatically_managed_folders"), + value: @object.automatically_managed? ? t(:"storages.label_active") : t(:"storages.label_inactive") + ) %> + + <% if @object.automatically_managed? %> <% component.with_attribute( - key: t(:"storages.label_managed_project_folders.automatically_managed_folders"), - value: @object.automatically_managed? ? t(:"storages.label_active") : t(:"storages.label_inactive") + key: t(:"storages.label_managed_project_folders.application_password"), + value: "●●●●●●●●●●●●●●●●" ) %> - - <% if @object.automatically_managed? %> - <% component.with_attribute( - key: t(:"storages.label_managed_project_folders.application_password"), - value: "●●●●●●●●●●●●●●●●" - ) %> - <% end %> <% end %> <% end %> <% end %> + diff --git a/modules/storages/app/views/storages/project_settings/_project_folder_form.html.erb b/modules/storages/app/views/storages/project_settings/_project_folder_form.html.erb index ec8ae2b38d5..2a06dde01bb 100644 --- a/modules/storages/app/views/storages/project_settings/_project_folder_form.html.erb +++ b/modules/storages/app/views/storages/project_settings/_project_folder_form.html.erb @@ -58,108 +58,105 @@ See COPYRIGHT and LICENSE files for more details.
    -<% if OpenProject::FeatureDecisions.storage_project_folders_active? %> - <%= f.hidden_field :project_folder_id, - data: { - 'project-storage-form-target': "projectFolderIdInput" - } %> -
    - <%= styled_label_tag :project_folder_mode, - class: "-bold -flex" do %> - <%= t(:"storages.label_project_folder") %> - <%= angular_component_tag 'op-static-attribute-help-text', - inputs: { - title: t(:"storages.label_project_folder"), - content: t(:"storages.help_texts.project_folder"), - } - %> - <% end %> -
    - <%= f.label :project_folder_mode, class: "form--label-with-radio-button" do %> - <%= f.radio_button :project_folder_mode, - 'inactive', - no_label: true, - data: { action: 'project-storage-form#updateForm' } %> - <%= t(:"storages.label_no_specific_folder") %> - <% end %> -
    - - <%= t(:"storages.instructions.no_specific_folder") %> - +<%= f.hidden_field :project_folder_id, + data: { + 'project-storage-form-target': "projectFolderIdInput" + } %> - <% if OpenProject::FeatureDecisions.managed_project_folders_active? %> -
    - <%= f.label :project_folder_mode, class: "form--label-with-radio-button" do %> - <%= f.radio_button :project_folder_mode, - 'automatic', - no_label: true, - data: { action: 'project-storage-form#updateForm' } %> - <%= t(:"storages.label_automatic_folder") %> - <% end %> -
    - - <%= t(:"storages.instructions.automatic_folder") %> - - <% end %> - -
    - <%= f.label :project_folder_mode, class: "form--label-with-radio-button" do %> - <%= f.radio_button :project_folder_mode, - 'manual', - no_label: true, - data: { action: 'project-storage-form#updateForm' } %> - <%= t(:"storages.label_existing_manual_folder") %> - <% end %> -
    - - <%= t(:"storages.instructions.existing_manual_folder") %> - - - - <% if @errors&.any? { |error| error.attribute == :project_folder_id } %> - <%= content_tag :span, - t(:"storages.instructions.empty_project_folder_validation"), - class: 'form--field-error -with-bottom-spacing', - data: { - 'project-storage-form-target': "projectFolderIdValidation" - } %> +
    + <%= styled_label_tag :project_folder_mode, + class: "-bold -flex" do %> + <%= t(:"storages.label_project_folder") %> + <%= angular_component_tag 'op-static-attribute-help-text', + inputs: { + title: t(:"storages.label_project_folder"), + content: t(:"storages.help_texts.project_folder"), + } + %> + <% end %> +
    + <%= f.label :project_folder_mode, class: "form--label-with-radio-button" do %> + <%= f.radio_button :project_folder_mode, + 'inactive', + no_label: true, + data: { action: 'project-storage-form#updateForm' } %> + <%= t(:"storages.label_no_specific_folder") %> <% end %>
    -<% end %> + + <%= t(:"storages.instructions.no_specific_folder") %> + + +
    + <%= f.label :project_folder_mode, class: "form--label-with-radio-button" do %> + <%= f.radio_button :project_folder_mode, + 'automatic', + no_label: true, + data: { action: 'project-storage-form#updateForm' } %> + <%= t(:"storages.label_automatic_folder") %> + <% end %> +
    + + <%= t(:"storages.instructions.automatic_folder") %> + + +
    + <%= f.label :project_folder_mode, class: "form--label-with-radio-button" do %> + <%= f.radio_button :project_folder_mode, + 'manual', + no_label: true, + data: { action: 'project-storage-form#updateForm' } %> + <%= t(:"storages.label_existing_manual_folder") %> + <% end %> +
    + + <%= t(:"storages.instructions.existing_manual_folder") %> + + + + <% if @errors&.any? { |error| error.attribute == :project_folder_id } %> + <%= content_tag :span, + t(:"storages.instructions.empty_project_folder_validation"), + class: 'form--field-error -with-bottom-spacing', + data: { + 'project-storage-form-target': "projectFolderIdValidation" + } %> + <% end %> +
    <%= styled_button_tag class: "-highlight" do %> <%= spot_icon('checkmark') %> <%= content_tag :span, submit_button_label %> <% end %> - <%= link_to project_settings_projects_storages_path(@project), class: 'button' do %> + <%= link_to project_settings_project_storages_path(@project), class: 'button' do %> <%= spot_icon('cancel') %> <%= content_tag :span, t(:button_cancel) %> <% end %> diff --git a/modules/storages/app/views/storages/project_settings/_storage_select_form.html.erb b/modules/storages/app/views/storages/project_settings/_storage_select_form.html.erb index 505382c004d..e894c558743 100644 --- a/modules/storages/app/views/storages/project_settings/_storage_select_form.html.erb +++ b/modules/storages/app/views/storages/project_settings/_storage_select_form.html.erb @@ -48,7 +48,7 @@ See COPYRIGHT and LICENSE files for more details. <%= spot_icon('checkmark') %> <%= content_tag :span, submit_button_label %> <% end %> - <%= link_to project_settings_projects_storages_path(@project), class: 'button' do %> + <%= link_to project_settings_project_storages_path(@project), class: 'button' do %> <%= spot_icon('cancel') %> <%= content_tag :span, t(:button_cancel) %> <% end %> diff --git a/modules/storages/app/views/storages/project_settings/destroy_info.html.erb b/modules/storages/app/views/storages/project_settings/destroy_info.html.erb index 5fe34316a0a..66ab18ca7d0 100644 --- a/modules/storages/app/views/storages/project_settings/destroy_info.html.erb +++ b/modules/storages/app/views/storages/project_settings/destroy_info.html.erb @@ -26,7 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> -<%= styled_form_tag(project_settings_projects_storage_path(project_id: @project_storage_to_destroy.project, id: @project_storage_to_destroy), +<%= styled_form_tag(project_settings_project_storage_path(project_id: @project_storage_to_destroy.project, id: @project_storage_to_destroy), class: 'danger-zone', method: :delete) do %>
    @@ -36,7 +36,7 @@ See COPYRIGHT and LICENSE files for more details.

    <%= t('storages.delete_warning.project_storage', file_storage: "#{h(@project_storage_to_destroy.storage.name)}").html_safe %>

    -
      +
      • <%= t('storages.delete_warning.project_storage_delete_result_1') %>
      • <%= t('storages.delete_warning.project_storage_delete_result_2') %>
      @@ -53,7 +53,7 @@ See COPYRIGHT and LICENSE files for more details. concat content_tag :i, '', class: 'button--icon icon-delete' concat content_tag :span, t(:button_delete), class: 'button--text' end %> - <%= link_to project_settings_projects_storages_path(@project_storage_to_destroy.project), + <%= link_to project_settings_project_storages_path(@project_storage_to_destroy.project), title: t(:button_cancel), class: 'button -with-icon icon-cancel' do %> <%= t(:button_cancel) %> diff --git a/modules/storages/app/views/storages/project_settings/edit.html.erb b/modules/storages/app/views/storages/project_settings/edit.html.erb index 78a69cd1339..38a69a3438b 100644 --- a/modules/storages/app/views/storages/project_settings/edit.html.erb +++ b/modules/storages/app/views/storages/project_settings/edit.html.erb @@ -31,7 +31,7 @@ See COPYRIGHT and LICENSE files for more details. <% local_assigns[:additional_breadcrumb] = t('storages.label_edit_storage') %> <%= toolbar title: t("storages.page_titles.project_settings.edit") %> -<%= labelled_tabular_form_for @project_storage, url: project_settings_projects_storage_path(project_id: @project_storage.project, id: @project_storage) do |f| -%> +<%= labelled_tabular_form_for @project_storage, url: project_settings_project_storage_path(project_id: @project_storage.project, id: @project_storage) do |f| -%>
      <%= render partial: '/storages/project_settings/project_folder_form', locals: { f: f, available_storages: @available_storages, submit_button_label: t('button_save') } diff --git a/modules/storages/app/views/storages/project_settings/index.html.erb b/modules/storages/app/views/storages/project_settings/index.html.erb index 379505112bc..a0e62398e73 100644 --- a/modules/storages/app/views/storages/project_settings/index.html.erb +++ b/modules/storages/app/views/storages/project_settings/index.html.erb @@ -33,7 +33,7 @@ See COPYRIGHT and LICENSE files for more details. <%= toolbar title: t("storages.page_titles.project_settings.index") do %>
    • - <%= link_to new_project_settings_projects_storage_path, + <%= link_to new_project_settings_project_storage_path, { class: 'button -alt-highlight', aria: { label: t(:'storages.label_new_storage') }, title: t(:'ifc_models.label_new_storage') } do %> @@ -43,7 +43,7 @@ See COPYRIGHT and LICENSE files for more details.
    • <% end %> - <%= render(::Storages::ProjectsStorages::TableComponent.new(rows: @projects_storages)) %> + <%= render(::Storages::ProjectStorages::TableComponent.new(rows: @project_storages)) %> <% else %> <%= render partial: '/storages/project_settings/toast_no_storage_set_up' %> <% end %> diff --git a/modules/storages/app/views/storages/project_settings/new.html.erb b/modules/storages/app/views/storages/project_settings/new.html.erb index cf5f8ee2b85..fa548b44bd3 100644 --- a/modules/storages/app/views/storages/project_settings/new.html.erb +++ b/modules/storages/app/views/storages/project_settings/new.html.erb @@ -35,14 +35,14 @@ See COPYRIGHT and LICENSE files for more details. <% if @available_storages.any? %> <% if @project_storage.storage.blank? %> <%= labelled_tabular_form_for @project_storage, - url: new_project_settings_projects_storage_path, + url: new_project_settings_project_storage_path, html: { method: :get } do |f| -%> <%= render partial: '/storages/project_settings/storage_select_form', locals: { f: f, available_storages: @available_storages, submit_button_label: t('button_continue') } %> <% end %> <% else %> - <%= labelled_tabular_form_for @project_storage, url: project_settings_projects_storages_path do |f| -%> + <%= labelled_tabular_form_for @project_storage, url: project_settings_project_storages_path do |f| -%> <%= render partial: '/storages/project_settings/project_folder_form', locals: { f: f, submit_button_label: t('button_add') } %> diff --git a/modules/storages/config/locales/crowdin/af.yml b/modules/storages/config/locales/crowdin/af.yml index 3ce44041d8c..c7800544ce9 100644 --- a/modules/storages/config/locales/crowdin/af.yml +++ b/modules/storages/config/locales/crowdin/af.yml @@ -16,7 +16,7 @@ af: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ af: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/ar.yml b/modules/storages/config/locales/crowdin/ar.yml index 71c1d036db3..8ad72a73257 100644 --- a/modules/storages/config/locales/crowdin/ar.yml +++ b/modules/storages/config/locales/crowdin/ar.yml @@ -16,7 +16,7 @@ ar: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ ar: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/az.yml b/modules/storages/config/locales/crowdin/az.yml index 9637983e99d..4b0fa8b4df4 100644 --- a/modules/storages/config/locales/crowdin/az.yml +++ b/modules/storages/config/locales/crowdin/az.yml @@ -16,7 +16,7 @@ az: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ az: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/be.yml b/modules/storages/config/locales/crowdin/be.yml index 1a0a278bf67..70bd7bfda7d 100644 --- a/modules/storages/config/locales/crowdin/be.yml +++ b/modules/storages/config/locales/crowdin/be.yml @@ -16,7 +16,7 @@ be: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Сховішча" attributes: storages/storage: @@ -50,6 +50,7 @@ be: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Гатова. Працягнуць наладку" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/bg.yml b/modules/storages/config/locales/crowdin/bg.yml index 5a376c30866..1ba494ad812 100644 --- a/modules/storages/config/locales/crowdin/bg.yml +++ b/modules/storages/config/locales/crowdin/bg.yml @@ -16,7 +16,7 @@ bg: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ bg: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/ca.yml b/modules/storages/config/locales/crowdin/ca.yml index b058a15b1b9..a59f527b0e4 100644 --- a/modules/storages/config/locales/crowdin/ca.yml +++ b/modules/storages/config/locales/crowdin/ca.yml @@ -16,7 +16,7 @@ ca: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Emmagatzematge" attributes: storages/storage: @@ -50,6 +50,7 @@ ca: errors: too_many_elements_created_at_once: "Masses elements creats alhora. S'esperen %{max} com a màxim, i s'han obtingut %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Fet. Continua la instal·lació" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/ckb-IR.yml b/modules/storages/config/locales/crowdin/ckb-IR.yml index a89c1aca0b7..c1f099dbd11 100644 --- a/modules/storages/config/locales/crowdin/ckb-IR.yml +++ b/modules/storages/config/locales/crowdin/ckb-IR.yml @@ -16,7 +16,7 @@ ckb-IR: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ ckb-IR: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/cs.yml b/modules/storages/config/locales/crowdin/cs.yml index ae1e3198c13..6193572252b 100644 --- a/modules/storages/config/locales/crowdin/cs.yml +++ b/modules/storages/config/locales/crowdin/cs.yml @@ -16,7 +16,7 @@ cs: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "Odkaz na soubor" + file_link: "File" storages/storage: "Úložiště" attributes: storages/storage: @@ -50,6 +50,7 @@ cs: errors: too_many_elements_created_at_once: "Příliš mnoho prvků vytvořených najednou. Očekáváno nanejvýš %{max} , získáno %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Hotovo. Pokračovat v nastavení" done_complete_setup: "Hotovo, kompletní nastavení" @@ -69,7 +70,7 @@ cs: index: "Úložiště souborů v tomto projektu" new: "Přidat úložiště souborů k tomuto projektu" edit: "Upravit úložiště souborů pro tento projekt" - delete: "Delete file storage" + delete: "Odstranit úložiště souborů" instructions: type: "Ujistěte se prosím, že máte oprávnění administrace v Nextcloud instanci a máte nainstalovanou následující aplikaci:" type_link_text: "„Integration OpenProject“" @@ -100,10 +101,10 @@ cs: Jste si jisti, že chcete smazat toto úložiště? Toto také odstraní úložiště ze všech projektů, kde je použito. Dále také smaže všechny odkazy z pracovních balíčků do souborů, které jsou uloženy v úložišti. project_storage: > Are you sure you want to delete %{file_storage} from this project? To confirm this action please introduce the storage name in the field below, this will: - project_storage_delete_result_1: "Remove all links from work packages of this project to files and folders of that storage." + project_storage_delete_result_1: "Odstraňte všechny odkazy z pracovních balíčků tohoto projektu do souborů a složek tohoto úložiště." project_storage_delete_result_2: "In case this storage has an automatically managed project folder, this and its files will be deleted forever." input_delete_confirmation: "Enter the file storage name %{file_storage} to confirm deletion." - irreversible_notice: "Deleting a file storage is an irreversible action." + irreversible_notice: "Smazání úložiště souborů je nevratná akce." label_active: "Aktivní" label_inactive: "Neaktivní" label_creator: "Vytvořil" diff --git a/modules/storages/config/locales/crowdin/da.yml b/modules/storages/config/locales/crowdin/da.yml index e2ee2179fb7..8902b22efb5 100644 --- a/modules/storages/config/locales/crowdin/da.yml +++ b/modules/storages/config/locales/crowdin/da.yml @@ -16,7 +16,7 @@ da: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ da: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/de.yml b/modules/storages/config/locales/crowdin/de.yml index c6bdfd6844b..e0864ad1ac8 100644 --- a/modules/storages/config/locales/crowdin/de.yml +++ b/modules/storages/config/locales/crowdin/de.yml @@ -16,7 +16,7 @@ de: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Speicher" attributes: storages/storage: @@ -50,6 +50,7 @@ de: errors: too_many_elements_created_at_once: "Zu viele Elemente gleichzeitig erstellt. Maximal %{max} erwartet, %{actual} bekommen." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Fertig. Setup fortsetzen" done_complete_setup: "Fertig, Einrichtung abgeschlossen" diff --git a/modules/storages/config/locales/crowdin/el.yml b/modules/storages/config/locales/crowdin/el.yml index ccaf14a022f..1d6f31f3c11 100644 --- a/modules/storages/config/locales/crowdin/el.yml +++ b/modules/storages/config/locales/crowdin/el.yml @@ -16,7 +16,7 @@ el: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ el: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/eo.yml b/modules/storages/config/locales/crowdin/eo.yml index 3e644df901a..2fad560ed48 100644 --- a/modules/storages/config/locales/crowdin/eo.yml +++ b/modules/storages/config/locales/crowdin/eo.yml @@ -16,7 +16,7 @@ eo: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ eo: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/es.yml b/modules/storages/config/locales/crowdin/es.yml index 747c53c92f1..6055ab7f32d 100644 --- a/modules/storages/config/locales/crowdin/es.yml +++ b/modules/storages/config/locales/crowdin/es.yml @@ -16,7 +16,7 @@ es: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Almacén" attributes: storages/storage: @@ -50,6 +50,7 @@ es: errors: too_many_elements_created_at_once: "Se intentaron crear demasiados elementos a la vez. Se esperaban como máximo %{max}, pero se recibieron %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Hecho. Continuar configuración" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/et.yml b/modules/storages/config/locales/crowdin/et.yml index a574dc82588..b3a6655337e 100644 --- a/modules/storages/config/locales/crowdin/et.yml +++ b/modules/storages/config/locales/crowdin/et.yml @@ -16,7 +16,7 @@ et: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ et: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/eu.yml b/modules/storages/config/locales/crowdin/eu.yml index 7f96157832b..e4fdfb27425 100644 --- a/modules/storages/config/locales/crowdin/eu.yml +++ b/modules/storages/config/locales/crowdin/eu.yml @@ -16,7 +16,7 @@ eu: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ eu: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/fa.yml b/modules/storages/config/locales/crowdin/fa.yml index 6ef298195ad..c8c6f7a2c6a 100644 --- a/modules/storages/config/locales/crowdin/fa.yml +++ b/modules/storages/config/locales/crowdin/fa.yml @@ -16,7 +16,7 @@ fa: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ fa: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/fi.yml b/modules/storages/config/locales/crowdin/fi.yml index e666a3f149a..4b46c7fc9d8 100644 --- a/modules/storages/config/locales/crowdin/fi.yml +++ b/modules/storages/config/locales/crowdin/fi.yml @@ -16,7 +16,7 @@ fi: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ fi: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/fil.yml b/modules/storages/config/locales/crowdin/fil.yml index c39cb2e8471..680f88c5bae 100644 --- a/modules/storages/config/locales/crowdin/fil.yml +++ b/modules/storages/config/locales/crowdin/fil.yml @@ -16,7 +16,7 @@ fil: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ fil: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/fr.yml b/modules/storages/config/locales/crowdin/fr.yml index 28e3ae2c3ec..5c84e6dea4a 100644 --- a/modules/storages/config/locales/crowdin/fr.yml +++ b/modules/storages/config/locales/crowdin/fr.yml @@ -16,7 +16,7 @@ fr: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Stockage" attributes: storages/storage: @@ -50,6 +50,7 @@ fr: errors: too_many_elements_created_at_once: "Trop d'éléments créés à la fois. %{max} attendu au maximum, obtenu %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Terminé. Poursuivre la configuration" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/he.yml b/modules/storages/config/locales/crowdin/he.yml index 68aef08731f..3b7e5916757 100644 --- a/modules/storages/config/locales/crowdin/he.yml +++ b/modules/storages/config/locales/crowdin/he.yml @@ -16,7 +16,7 @@ he: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ he: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/hi.yml b/modules/storages/config/locales/crowdin/hi.yml index 430b062c33a..1860ac0e026 100644 --- a/modules/storages/config/locales/crowdin/hi.yml +++ b/modules/storages/config/locales/crowdin/hi.yml @@ -16,7 +16,7 @@ hi: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ hi: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/hr.yml b/modules/storages/config/locales/crowdin/hr.yml index a1299172261..b147840ef7a 100644 --- a/modules/storages/config/locales/crowdin/hr.yml +++ b/modules/storages/config/locales/crowdin/hr.yml @@ -16,7 +16,7 @@ hr: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ hr: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/hu.yml b/modules/storages/config/locales/crowdin/hu.yml index 0396783cefd..fdce5a1b984 100644 --- a/modules/storages/config/locales/crowdin/hu.yml +++ b/modules/storages/config/locales/crowdin/hu.yml @@ -16,7 +16,7 @@ hu: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Tárhely" attributes: storages/storage: @@ -50,6 +50,7 @@ hu: errors: too_many_elements_created_at_once: "Egyszerre túl sok elem létrehozása történt. Legfejlebb %{max} lehet, ténylegesen %{actual} volt." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/id.yml b/modules/storages/config/locales/crowdin/id.yml index 490f17e47de..9cc310ab660 100644 --- a/modules/storages/config/locales/crowdin/id.yml +++ b/modules/storages/config/locales/crowdin/id.yml @@ -16,7 +16,7 @@ id: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Penyimpanan" attributes: storages/storage: @@ -50,6 +50,7 @@ id: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/it.yml b/modules/storages/config/locales/crowdin/it.yml index c9c8c95788d..256f6d96714 100644 --- a/modules/storages/config/locales/crowdin/it.yml +++ b/modules/storages/config/locales/crowdin/it.yml @@ -16,7 +16,7 @@ it: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Archivio" attributes: storages/storage: @@ -50,6 +50,7 @@ it: errors: too_many_elements_created_at_once: "Troppi elementi creati contemporaneamente. Massimo atteso: %{max}, effettivi: %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Fatto. Continua la configurazione" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/ja.yml b/modules/storages/config/locales/crowdin/ja.yml index 0c492036a7c..060316f11b8 100644 --- a/modules/storages/config/locales/crowdin/ja.yml +++ b/modules/storages/config/locales/crowdin/ja.yml @@ -16,7 +16,7 @@ ja: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ ja: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/js-af.yml b/modules/storages/config/locales/crowdin/js-af.yml index 76c23581c6f..8a2ecc186bb 100644 --- a/modules/storages/config/locales/crowdin/js-af.yml +++ b/modules/storages/config/locales/crowdin/js-af.yml @@ -31,6 +31,7 @@ af: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-ar.yml b/modules/storages/config/locales/crowdin/js-ar.yml index 72765fb7d23..6bd6bb452f5 100644 --- a/modules/storages/config/locales/crowdin/js-ar.yml +++ b/modules/storages/config/locales/crowdin/js-ar.yml @@ -31,6 +31,7 @@ ar: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-az.yml b/modules/storages/config/locales/crowdin/js-az.yml index 432887a45df..3311a4c6c67 100644 --- a/modules/storages/config/locales/crowdin/js-az.yml +++ b/modules/storages/config/locales/crowdin/js-az.yml @@ -31,6 +31,7 @@ az: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-be.yml b/modules/storages/config/locales/crowdin/js-be.yml index 1108bec946a..e6edb0b5f5d 100644 --- a/modules/storages/config/locales/crowdin/js-be.yml +++ b/modules/storages/config/locales/crowdin/js-be.yml @@ -31,6 +31,7 @@ be: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-bg.yml b/modules/storages/config/locales/crowdin/js-bg.yml index 12cdcc0bf58..a837c4cb988 100644 --- a/modules/storages/config/locales/crowdin/js-bg.yml +++ b/modules/storages/config/locales/crowdin/js-bg.yml @@ -31,6 +31,7 @@ bg: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-ca.yml b/modules/storages/config/locales/crowdin/js-ca.yml index f4c5d234e81..eb0b991ba5d 100644 --- a/modules/storages/config/locales/crowdin/js-ca.yml +++ b/modules/storages/config/locales/crowdin/js-ca.yml @@ -31,6 +31,7 @@ ca: Un fitxer amb el nom "%{fileName}" ja existeix a la ubicació on estàs intentant carregar-lo. Que vols fer? directory_not_writeable: "No tens permís per afegir fitxers dins d'aquesta carpeta." dragging_many_files: "La càrrega a %{storageType} només permet un fitxer per càrrega." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "Aquesta carpeta està buida." empty_folder_location_hint: "Fes clic al botó de sota per carregar fitxers a aquesta ubicació." file_not_selectable_location: "Seleccionar un fitxer, no és possible en el procés de tria d'ubicació." diff --git a/modules/storages/config/locales/crowdin/js-ckb-IR.yml b/modules/storages/config/locales/crowdin/js-ckb-IR.yml index 746c59edb9a..43389b5f8d9 100644 --- a/modules/storages/config/locales/crowdin/js-ckb-IR.yml +++ b/modules/storages/config/locales/crowdin/js-ckb-IR.yml @@ -31,6 +31,7 @@ ckb-IR: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-cs.yml b/modules/storages/config/locales/crowdin/js-cs.yml index 10ed4a37d5b..0bcb69ddb11 100644 --- a/modules/storages/config/locales/crowdin/js-cs.yml +++ b/modules/storages/config/locales/crowdin/js-cs.yml @@ -6,7 +6,7 @@ cs: link_existing_files: "Odkaz na existující soubory" upload_files: "Nahrát soubory" drop_files: "Přetáhněte soubory pro jejich nahrání na %{name}." - drop_or_click_files: "Drop files here or click to upload them to %{name}." + drop_or_click_files: "Přetáhněte soubory sem nebo klikněte pro jejich nahrání do %{name}." login: "%{storageType} přihlášení" login_to: "Přihlásit se k %{storageType}" no_connection: "Žádné připojení %{storageType}" @@ -31,6 +31,7 @@ cs: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "Nemáte oprávnění přidávat soubory do této složky." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "Tato složka je prázdná." empty_folder_location_hint: "Klikněte na tlačítko níže pro nahrání souboru do tohoto umístění." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-da.yml b/modules/storages/config/locales/crowdin/js-da.yml index a0c9df8db02..b44984c4353 100644 --- a/modules/storages/config/locales/crowdin/js-da.yml +++ b/modules/storages/config/locales/crowdin/js-da.yml @@ -31,6 +31,7 @@ da: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-de.yml b/modules/storages/config/locales/crowdin/js-de.yml index b6c6b24ef9c..7e37f488d8e 100644 --- a/modules/storages/config/locales/crowdin/js-de.yml +++ b/modules/storages/config/locales/crowdin/js-de.yml @@ -31,6 +31,7 @@ de: Eine Datei mit dem Namen „%{fileName}“ existiert bereits an dem Ort, an dem Sie versuchen, diese Datei hochzuladen. Was möchten Sie tun? directory_not_writeable: "Sie haben keine Berechtigung, Dateien zu diesem Ordner hinzuzufügen." dragging_many_files: "Das Hochladen auf %{storageType} unterstützt nur eine Datei auf einmal." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "Dieser Ordner ist leer." empty_folder_location_hint: "Klicken Sie auf die Schaltfläche unten, um die Datei an diesen Ort hochzuladen." file_not_selectable_location: "Die Auswahl einer Datei ist nicht möglich, wenn Sie einen Speicherort auswählen." diff --git a/modules/storages/config/locales/crowdin/js-el.yml b/modules/storages/config/locales/crowdin/js-el.yml index b637cc24b1d..745e95ef20d 100644 --- a/modules/storages/config/locales/crowdin/js-el.yml +++ b/modules/storages/config/locales/crowdin/js-el.yml @@ -31,6 +31,7 @@ el: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-eo.yml b/modules/storages/config/locales/crowdin/js-eo.yml index 53e9db42b44..92b536b4d06 100644 --- a/modules/storages/config/locales/crowdin/js-eo.yml +++ b/modules/storages/config/locales/crowdin/js-eo.yml @@ -31,6 +31,7 @@ eo: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-es.yml b/modules/storages/config/locales/crowdin/js-es.yml index c21ba3261a9..12f5424517e 100644 --- a/modules/storages/config/locales/crowdin/js-es.yml +++ b/modules/storages/config/locales/crowdin/js-es.yml @@ -31,6 +31,7 @@ es: Ya existe un archivo con el nombre "%{fileName}" en la ubicación donde está intentando subir este archivo. ¿Qué quiere hacer? directory_not_writeable: "No tiene permiso para añadir archivos en esta carpeta." dragging_many_files: "La carga a %{storageType} solo admite un archivo a la vez." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "Esta carpeta está vacía." empty_folder_location_hint: "Haga clic en el botón de abajo para subir el archivo a esta ubicación." file_not_selectable_location: "No es posible seleccionar un archivo en el proceso de seleccionar una ubicación." diff --git a/modules/storages/config/locales/crowdin/js-et.yml b/modules/storages/config/locales/crowdin/js-et.yml index d8b3933725d..3239d9dc0dc 100644 --- a/modules/storages/config/locales/crowdin/js-et.yml +++ b/modules/storages/config/locales/crowdin/js-et.yml @@ -31,6 +31,7 @@ et: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-eu.yml b/modules/storages/config/locales/crowdin/js-eu.yml index e98ca793095..93b6cd76af7 100644 --- a/modules/storages/config/locales/crowdin/js-eu.yml +++ b/modules/storages/config/locales/crowdin/js-eu.yml @@ -31,6 +31,7 @@ eu: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-fa.yml b/modules/storages/config/locales/crowdin/js-fa.yml index ac82172d50e..f6c313764d3 100644 --- a/modules/storages/config/locales/crowdin/js-fa.yml +++ b/modules/storages/config/locales/crowdin/js-fa.yml @@ -31,6 +31,7 @@ fa: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-fi.yml b/modules/storages/config/locales/crowdin/js-fi.yml index 6d33ed7c53e..efefd1ffbd5 100644 --- a/modules/storages/config/locales/crowdin/js-fi.yml +++ b/modules/storages/config/locales/crowdin/js-fi.yml @@ -31,6 +31,7 @@ fi: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-fil.yml b/modules/storages/config/locales/crowdin/js-fil.yml index bc4f2ab201b..8cb4c2c474a 100644 --- a/modules/storages/config/locales/crowdin/js-fil.yml +++ b/modules/storages/config/locales/crowdin/js-fil.yml @@ -31,6 +31,7 @@ fil: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-fr.yml b/modules/storages/config/locales/crowdin/js-fr.yml index 092907eb20b..ea882dd87fd 100644 --- a/modules/storages/config/locales/crowdin/js-fr.yml +++ b/modules/storages/config/locales/crowdin/js-fr.yml @@ -31,6 +31,7 @@ fr: Un fichier avec le nom « %{fileName} » existe déjà à l'emplacement où vous essayez de téléverser ce fichier. Que voulez-vous faire ? directory_not_writeable: "Vous n'avez pas la permission d'ajouter des fichiers à ce dossier." dragging_many_files: "Le téléversement vers %{storageType} ne supporte qu'un seul fichier à la fois." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "Ce dossier est vide." empty_folder_location_hint: "Cliquez sur le bouton ci-dessous pour téléverser le fichier à cet endroit." file_not_selectable_location: "La sélection d'un fichier est impossible pendant le processus de sélection d'un emplacement." diff --git a/modules/storages/config/locales/crowdin/js-he.yml b/modules/storages/config/locales/crowdin/js-he.yml index 4bef7be0af8..93cc899beba 100644 --- a/modules/storages/config/locales/crowdin/js-he.yml +++ b/modules/storages/config/locales/crowdin/js-he.yml @@ -31,6 +31,7 @@ he: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-hi.yml b/modules/storages/config/locales/crowdin/js-hi.yml index 66d18f57778..6f35ea9f9e3 100644 --- a/modules/storages/config/locales/crowdin/js-hi.yml +++ b/modules/storages/config/locales/crowdin/js-hi.yml @@ -31,6 +31,7 @@ hi: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-hr.yml b/modules/storages/config/locales/crowdin/js-hr.yml index 75792ace891..7ff689c9ac3 100644 --- a/modules/storages/config/locales/crowdin/js-hr.yml +++ b/modules/storages/config/locales/crowdin/js-hr.yml @@ -31,6 +31,7 @@ hr: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-hu.yml b/modules/storages/config/locales/crowdin/js-hu.yml index 6615bf85876..d0f1d6a6e94 100644 --- a/modules/storages/config/locales/crowdin/js-hu.yml +++ b/modules/storages/config/locales/crowdin/js-hu.yml @@ -31,6 +31,7 @@ hu: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-id.yml b/modules/storages/config/locales/crowdin/js-id.yml index 2f62a77817f..4bea364a252 100644 --- a/modules/storages/config/locales/crowdin/js-id.yml +++ b/modules/storages/config/locales/crowdin/js-id.yml @@ -31,6 +31,7 @@ id: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-it.yml b/modules/storages/config/locales/crowdin/js-it.yml index adbfc1f1c8d..f49d3faab7e 100644 --- a/modules/storages/config/locales/crowdin/js-it.yml +++ b/modules/storages/config/locales/crowdin/js-it.yml @@ -31,6 +31,7 @@ it: Un file con il nome "%{fileName}" esiste già nella posizione in cui stai tentando di caricare questo file. Cosa ti piacerebbe fare? directory_not_writeable: "Non hai l'autorizzazione ad aggiungere file a questa cartella." dragging_many_files: "Il caricamento su %{storageType} supporta soltanto un file per volta." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "Questa cartella è vuota." empty_folder_location_hint: "Clicca sul pulsante in basso per caricare il file in questa posizione." file_not_selectable_location: "Impossibile selezionare un file durante la scelta di una posizione." diff --git a/modules/storages/config/locales/crowdin/js-ja.yml b/modules/storages/config/locales/crowdin/js-ja.yml index 5f6508ee752..1b2547f9e6c 100644 --- a/modules/storages/config/locales/crowdin/js-ja.yml +++ b/modules/storages/config/locales/crowdin/js-ja.yml @@ -31,6 +31,7 @@ ja: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-ka.yml b/modules/storages/config/locales/crowdin/js-ka.yml index f42cdbe0461..d177d03ddf4 100644 --- a/modules/storages/config/locales/crowdin/js-ka.yml +++ b/modules/storages/config/locales/crowdin/js-ka.yml @@ -31,6 +31,7 @@ ka: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-ko.yml b/modules/storages/config/locales/crowdin/js-ko.yml index 388ec47f7d4..e086b5a435c 100644 --- a/modules/storages/config/locales/crowdin/js-ko.yml +++ b/modules/storages/config/locales/crowdin/js-ko.yml @@ -31,6 +31,7 @@ ko: 이 파일을 업로드하려는 위치에 이름이 "%{fileName}"인 파일이 이미 있습니다. 어떤 작업을 하시겠습니까? directory_not_writeable: "이 폴더에 파일을 추가할 수 있는 권한이 없습니다." dragging_many_files: "%{storageType}(으)로 업로드는 한 번에 하나의 파일만 지원합니다." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "이 폴더는 비어 있습니다." empty_folder_location_hint: "아래 버튼을 클릭하여 이 위치에 파일을 업로드하세요." file_not_selectable_location: "위치 선택 프로세스에서 파일을 선택할 수는 없습니다." diff --git a/modules/storages/config/locales/crowdin/js-lt.yml b/modules/storages/config/locales/crowdin/js-lt.yml index a7054a26b78..83a3d40230c 100644 --- a/modules/storages/config/locales/crowdin/js-lt.yml +++ b/modules/storages/config/locales/crowdin/js-lt.yml @@ -31,6 +31,7 @@ lt: Failas pavadinimu „%{fileName}“ jau yra vietoje, į kurią bandote jį įkelti. Ką norėtumėte padaryti? directory_not_writeable: "Jūs neturite teisių pridėti failų į šį aplanką." dragging_many_files: "Įkėlimas į %{storageType} galimas tik po vieną failą vienu metu." + dragging_folder: "Įkėlimas į %{storageType} nepalaiko aplankų." empty_folder: "Šis aplankas yra tuščias." empty_folder_location_hint: "Spauskite mygtuką žemiau, jei norite įkelti failą į šią vietą." file_not_selectable_location: "Vietos pasirinkimo metu negalima parinkti failo." diff --git a/modules/storages/config/locales/crowdin/js-lv.yml b/modules/storages/config/locales/crowdin/js-lv.yml index c6401ff5941..d78a24ddee9 100644 --- a/modules/storages/config/locales/crowdin/js-lv.yml +++ b/modules/storages/config/locales/crowdin/js-lv.yml @@ -31,6 +31,7 @@ lv: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-mn.yml b/modules/storages/config/locales/crowdin/js-mn.yml index 34fad54ed80..23d6c5c4d72 100644 --- a/modules/storages/config/locales/crowdin/js-mn.yml +++ b/modules/storages/config/locales/crowdin/js-mn.yml @@ -31,6 +31,7 @@ mn: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-ne.yml b/modules/storages/config/locales/crowdin/js-ne.yml index 038666101db..bdc7cc980e9 100644 --- a/modules/storages/config/locales/crowdin/js-ne.yml +++ b/modules/storages/config/locales/crowdin/js-ne.yml @@ -31,6 +31,7 @@ ne: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-nl.yml b/modules/storages/config/locales/crowdin/js-nl.yml index d3115c17283..a10b9d322ff 100644 --- a/modules/storages/config/locales/crowdin/js-nl.yml +++ b/modules/storages/config/locales/crowdin/js-nl.yml @@ -4,15 +4,15 @@ nl: storages: link_files_in_storage: "Koppel bestanden aan %{storageType}" link_existing_files: "Bestaande bestanden koppelen" - upload_files: "Upload files" + upload_files: "Bestanden uploaden" drop_files: "Drop files here to upload them to %{name}." drop_or_click_files: "Drop files here or click to upload them to %{name}." login: "%{storageType} login" login_to: "Inloggen op %{storageType}" no_connection: "Geen %{storageType} verbinding" open_storage: "%{storageType} openen" - select_location: "Select location" - choose_location: "Choose location" + select_location: "Selecteer locatie" + choose_location: "Kies locatie" types: nextcloud: "Nextcloud" default: "Opslag" @@ -26,17 +26,18 @@ nl: not_logged_in: > Om een link toetevoegen, bekijk of upload bestanden gerelateerd aan dit werkpakket, log dan in op %{storageType}. files: - already_existing_header: "This file already exists" + already_existing_header: "Dit bestand bestaat al" already_existing_body: > A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." - empty_folder: "This folder is empty." - empty_folder_location_hint: "Click the button below to upload the file to this location." + dragging_folder: "The upload to %{storageType} does not support folders." + empty_folder: "Deze map is leeg." + empty_folder_location_hint: "Klik op de knop hieronder om het bestand te uploaden naar deze locatie." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." project_folder_no_access: > You have no access to the project folder. Please, contact your administrator to get access or upload the file in another location. - upload_keep_both: "Keep both" + upload_keep_both: "Beide behouden" upload_replace: "Vervang" file_links: empty: > @@ -53,8 +54,8 @@ nl: select_all: "Alles selecteren" selection: zero: "Selecteer bestanden om te koppelen" - one: "Link 1 file" - other: "Link %{count} files" + one: "Link 1 bestand" + other: "Link %{count} bestanden" success_create: one: "Successfully created 1 file link." other: "Successfully created %{count} file links." diff --git a/modules/storages/config/locales/crowdin/js-no.yml b/modules/storages/config/locales/crowdin/js-no.yml index 1f64ab15f89..70da0b6397a 100644 --- a/modules/storages/config/locales/crowdin/js-no.yml +++ b/modules/storages/config/locales/crowdin/js-no.yml @@ -31,6 +31,7 @@ A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-pl.yml b/modules/storages/config/locales/crowdin/js-pl.yml index 5ea80327a27..20401f78338 100644 --- a/modules/storages/config/locales/crowdin/js-pl.yml +++ b/modules/storages/config/locales/crowdin/js-pl.yml @@ -31,6 +31,7 @@ pl: Plik o nazwie „%{fileName}” już istnieje w lokalizacji, do której próbujesz go przesłać. Co chcesz zrobić? directory_not_writeable: "Nie masz uprawnień do dodawania plików do tego folderu." dragging_many_files: "Przesyłanie do %{storageType} obsługuje tylko jeden plik na raz." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "Ten folder jest pusty." empty_folder_location_hint: "Kliknij poniższy przycisk, aby przesłać plik do tej lokalizacji." file_not_selectable_location: "Wybranie pliku nie jest możliwe w procesie wyboru lokalizacji." diff --git a/modules/storages/config/locales/crowdin/js-pt.yml b/modules/storages/config/locales/crowdin/js-pt.yml index 734c473985d..8d8a9d2da86 100644 --- a/modules/storages/config/locales/crowdin/js-pt.yml +++ b/modules/storages/config/locales/crowdin/js-pt.yml @@ -31,6 +31,7 @@ pt: Já existe um arquivo com o nome "%{fileName}" na localização para onde você está tentando enviar este arquivo. O que você deseja fazer? directory_not_writeable: "Você não possui permissão para adicionar arquivos a esta pasta." dragging_many_files: "O carregamento para %{storageType} só dá suporte a um arquivo por vez." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "Esta pasta está vazia." empty_folder_location_hint: "Clique no botão abaixo para carregar o arquivo para esta localização." file_not_selectable_location: "Não é possível selecionar um arquivo no processo de escolha de uma localização." diff --git a/modules/storages/config/locales/crowdin/js-ro.yml b/modules/storages/config/locales/crowdin/js-ro.yml index f7400224eaa..3531beb9616 100644 --- a/modules/storages/config/locales/crowdin/js-ro.yml +++ b/modules/storages/config/locales/crowdin/js-ro.yml @@ -31,6 +31,7 @@ ro: Un fișier cu numele "%{fileName}" există deja în locația în care încercați să încărcați acest fișier. Ce ai vrea să faci? directory_not_writeable: "Nu aveţi permisiunea de a adăuga fişiere în acest folder." dragging_many_files: "Încărcarea către %{storageType} suportă doar un singur fișier simultan." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "Acest director este gol." empty_folder_location_hint: "Faceţi clic pe butonul de mai jos pentru a încărca fişierul în această locaţie." file_not_selectable_location: "Selectarea unui fișier nu este posibilă în procesul de alegere a unei locații." diff --git a/modules/storages/config/locales/crowdin/js-ru.yml b/modules/storages/config/locales/crowdin/js-ru.yml index 790b2aef8d4..0401affd098 100644 --- a/modules/storages/config/locales/crowdin/js-ru.yml +++ b/modules/storages/config/locales/crowdin/js-ru.yml @@ -31,6 +31,7 @@ ru: Файл с именем "%{fileName}" уже существует в папке, в которую вы пытаетесь загрузить этот файл. Что бы вы хотели сделать? directory_not_writeable: "У вас нет прав на добавление файлов в эту папку." dragging_many_files: "Загрузка в %{storageType} поддерживает только один файл одновременно." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "Папка пуста." empty_folder_location_hint: "Нажмите на кнопку ниже, чтобы загрузить файл в это место." file_not_selectable_location: "Выбор файла невозможен в процессе выбора местоположения." diff --git a/modules/storages/config/locales/crowdin/js-rw.yml b/modules/storages/config/locales/crowdin/js-rw.yml index 7524f8c5425..fb7f48fa052 100644 --- a/modules/storages/config/locales/crowdin/js-rw.yml +++ b/modules/storages/config/locales/crowdin/js-rw.yml @@ -31,6 +31,7 @@ rw: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-si.yml b/modules/storages/config/locales/crowdin/js-si.yml index 7ac58e854f5..1c7eeb5a895 100644 --- a/modules/storages/config/locales/crowdin/js-si.yml +++ b/modules/storages/config/locales/crowdin/js-si.yml @@ -31,6 +31,7 @@ si: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-sk.yml b/modules/storages/config/locales/crowdin/js-sk.yml index 8d969129668..e03f1774590 100644 --- a/modules/storages/config/locales/crowdin/js-sk.yml +++ b/modules/storages/config/locales/crowdin/js-sk.yml @@ -31,6 +31,7 @@ sk: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-sl.yml b/modules/storages/config/locales/crowdin/js-sl.yml index 1ef69f99f59..275fe6bb51c 100644 --- a/modules/storages/config/locales/crowdin/js-sl.yml +++ b/modules/storages/config/locales/crowdin/js-sl.yml @@ -31,6 +31,7 @@ sl: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-sr.yml b/modules/storages/config/locales/crowdin/js-sr.yml index 772759c2521..173d5825344 100644 --- a/modules/storages/config/locales/crowdin/js-sr.yml +++ b/modules/storages/config/locales/crowdin/js-sr.yml @@ -31,6 +31,7 @@ sr: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-sv.yml b/modules/storages/config/locales/crowdin/js-sv.yml index aa01c2a4490..27920cecdf2 100644 --- a/modules/storages/config/locales/crowdin/js-sv.yml +++ b/modules/storages/config/locales/crowdin/js-sv.yml @@ -31,6 +31,7 @@ sv: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-th.yml b/modules/storages/config/locales/crowdin/js-th.yml index 39c45882547..f454350370c 100644 --- a/modules/storages/config/locales/crowdin/js-th.yml +++ b/modules/storages/config/locales/crowdin/js-th.yml @@ -31,6 +31,7 @@ th: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-tr.yml b/modules/storages/config/locales/crowdin/js-tr.yml index 3e1e81e9602..91408e88c79 100644 --- a/modules/storages/config/locales/crowdin/js-tr.yml +++ b/modules/storages/config/locales/crowdin/js-tr.yml @@ -31,6 +31,7 @@ tr: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "%{storageType}'a yükleme aynı anda yalnızca bir dosyayı destekler." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "Bu klasör boş.\n" empty_folder_location_hint: "Dosyayı bu konuma yüklemek için aşağıdaki düğmeyi tıklayın." file_not_selectable_location: "Konum seçme sürecinde dosya seçmek mümkün değildir." diff --git a/modules/storages/config/locales/crowdin/js-uk.yml b/modules/storages/config/locales/crowdin/js-uk.yml index b9df59f3c4a..a5c9231ccda 100644 --- a/modules/storages/config/locales/crowdin/js-uk.yml +++ b/modules/storages/config/locales/crowdin/js-uk.yml @@ -31,6 +31,7 @@ uk: Файл із назвою «%{fileName}» уже існує в папці, у яку ви намагаєтеся вивантажити його. Що потрібно зробити? directory_not_writeable: "У вас немає дозволу на додання файлів у цю папку." dragging_many_files: "У сховище %{storageType} можна вивантажувати тільки один файл за раз." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "Ця папка пуста." empty_folder_location_hint: "Натисніть кнопку нижче, щоб вивантажити файл у цю папку." file_not_selectable_location: "Неможливо вибрати файл під час вибору папки." diff --git a/modules/storages/config/locales/crowdin/js-vi.yml b/modules/storages/config/locales/crowdin/js-vi.yml index 4c95ca55fb5..6f5b7033b07 100644 --- a/modules/storages/config/locales/crowdin/js-vi.yml +++ b/modules/storages/config/locales/crowdin/js-vi.yml @@ -31,6 +31,7 @@ vi: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/js-zh-TW.yml b/modules/storages/config/locales/crowdin/js-zh-TW.yml index ffe4dffc867..06a64051700 100644 --- a/modules/storages/config/locales/crowdin/js-zh-TW.yml +++ b/modules/storages/config/locales/crowdin/js-zh-TW.yml @@ -31,6 +31,7 @@ zh-TW: A file with the name "%{fileName}" already exists in the location where you are trying to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/locales/crowdin/ka.yml b/modules/storages/config/locales/crowdin/ka.yml index c8b1a4feed2..f8de9e44a06 100644 --- a/modules/storages/config/locales/crowdin/ka.yml +++ b/modules/storages/config/locales/crowdin/ka.yml @@ -16,7 +16,7 @@ ka: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ ka: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/ko.yml b/modules/storages/config/locales/crowdin/ko.yml index b741dbe0750..ac8d00c5ee2 100644 --- a/modules/storages/config/locales/crowdin/ko.yml +++ b/modules/storages/config/locales/crowdin/ko.yml @@ -16,7 +16,7 @@ ko: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "저장소" attributes: storages/storage: @@ -50,6 +50,7 @@ ko: errors: too_many_elements_created_at_once: "한 번에 너무 많은 요소가 생성되었습니다. 최대 개수는 %{max}개이지만 %{actual}개가 있습니다." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "완료. 설정을 계속하세요." done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/lt.yml b/modules/storages/config/locales/crowdin/lt.yml index afd1a6736ac..bc868603878 100644 --- a/modules/storages/config/locales/crowdin/lt.yml +++ b/modules/storages/config/locales/crowdin/lt.yml @@ -12,11 +12,11 @@ lt: errors: attributes: storage_error: - not_authorized: "Not authorized for external connection to storage." - not_found: "The requested resource could not be found at the external file storage." + not_authorized: "Neautorizuota išoriniam prisijungimui prie saugyklos." + not_found: "Prašytas resursas nerastas išorinėje failų saugykloje." activerecord: models: - file_link: "Failo nuoroda" + file_link: "Failas" storages/storage: "Saugykla" attributes: storages/storage: @@ -50,6 +50,7 @@ lt: errors: too_many_elements_created_at_once: "Per daug elementu vienu metu. Tikėtasi daugiausia %{max}, gauta %{actual}." storages: + unknown_storage: "Nežinoma saugykla" buttons: done_continue_setup: "Atlikta. Tęsti nustatymą" done_complete_setup: "Atlikta, baigti nustatymą" @@ -69,7 +70,7 @@ lt: index: "Šiame projekte prieinamos failų saugyklos." new: "Pridėti failų saugyklą šiam projektui" edit: "Keisti šio projekto failų saugyklą" - delete: "Delete file storage" + delete: "Trinti failų saugyklą" instructions: type: "Prašome įsitikinti, kad turite administratoriaus teises jūsų Nextcloud egzemplioriuje bei kad įdiegtos šios aplikacijos, prieš tęsiant nustatymą:" type_link_text: "„Integration OpenProject“" @@ -99,11 +100,11 @@ lt: storage: > Ar tikrai norite ištrinti šią failų saugyklą? Saugykla taipogi bus ištrinta iš visų projektų, kur ji naudojama. Taipogi tai ištrins visus ryšius iš darbo paketų į failus, esančius šioje saugykloje. project_storage: > - Are you sure you want to delete %{file_storage} from this project? To confirm this action please introduce the storage name in the field below, this will: - project_storage_delete_result_1: "Remove all links from work packages of this project to files and folders of that storage." - project_storage_delete_result_2: "In case this storage has an automatically managed project folder, this and its files will be deleted forever." - input_delete_confirmation: "Enter the file storage name %{file_storage} to confirm deletion." - irreversible_notice: "Deleting a file storage is an irreversible action." + Ar tikrai norite ištrinti %{file_storage} iš šio projekto? Šio veiksmo patvirtinimui prašome įvesti saugyklos pavadinimą lauke žemiau, tai: + project_storage_delete_result_1: "Išims iš šio projekto darbo paketų visas nuorodas į tos saugyklos failus ir aplankus." + project_storage_delete_result_2: "Jei saugykla turi automatiškai tvarkomą projekto aplanką, tai ir jo failai bus visam laikui ištrinti." + input_delete_confirmation: "Įveskite failų saugyklos pavadinimą %{file_storage}, kad patvirtintumėte trynimą." + irreversible_notice: "Failų saugyklos trynimas yra neatstatomas veiksmas." label_active: "Aktyvus" label_inactive: "Neaktyvus" label_creator: "Kūrėjas" diff --git a/modules/storages/config/locales/crowdin/lv.yml b/modules/storages/config/locales/crowdin/lv.yml index 8ea3a812303..6a4c49f1385 100644 --- a/modules/storages/config/locales/crowdin/lv.yml +++ b/modules/storages/config/locales/crowdin/lv.yml @@ -16,7 +16,7 @@ lv: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ lv: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/mn.yml b/modules/storages/config/locales/crowdin/mn.yml index dae03ae262c..d6810a14158 100644 --- a/modules/storages/config/locales/crowdin/mn.yml +++ b/modules/storages/config/locales/crowdin/mn.yml @@ -16,7 +16,7 @@ mn: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ mn: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/ne.yml b/modules/storages/config/locales/crowdin/ne.yml index a1bd2fdf39b..a5dd86ae940 100644 --- a/modules/storages/config/locales/crowdin/ne.yml +++ b/modules/storages/config/locales/crowdin/ne.yml @@ -16,7 +16,7 @@ ne: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ ne: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/nl.yml b/modules/storages/config/locales/crowdin/nl.yml index 4be18c9dc95..a2d515d1050 100644 --- a/modules/storages/config/locales/crowdin/nl.yml +++ b/modules/storages/config/locales/crowdin/nl.yml @@ -16,7 +16,7 @@ nl: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Opslagruimte" attributes: storages/storage: @@ -40,7 +40,7 @@ nl: authorization_header_missing: > is niet volledig ingesteld. De Nextcloud instantie ontvangt geen "Autorisatie" header, wat nodig is voor een Bearer token gebaseerd op API verzoeken. Controleer de configuratie van uw HTTP-server. password: - invalid_password: "is not valid." + invalid_password: "is niet geldig." unknown_error: "could not be validated. Please check your storage connection and try again." storages/file_link: attributes: @@ -50,6 +50,7 @@ nl: errors: too_many_elements_created_at_once: "Te veel elementen gemaakt tegelijk. %{max} verwacht hoogstens %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Klaar. Ga verder met instellen" done_complete_setup: "Done, complete setup" @@ -59,24 +60,24 @@ nl: save_and_continue_setup: "Opslaan en doorgaan met instellen" save_and_complete_setup: "Opslaan en de installatie voltooien" select_folder: "Selecteer map" - configure: "Configure" + configure: "Configureren" page_titles: managed_project_folders: - title: "Automatically managed project folders" + title: "Automatisch beheerde projectmappen" subtitle: > Let OpenProject create folders per project automatically. This is recommended as it ensures that every team member always has the correct access permissions. project_settings: index: "Bestandsopslag beschikbaar in dit project" new: "Voeg een bestandsopslag toe aan dit project" - edit: "Edit the file storage to this project" - delete: "Delete file storage" + edit: "Bewerk de bestandsopslag voor dit project" + delete: "Verwijder bestandsopslag" instructions: type: "Zorg ervoor dat u beheerrechten heeft in uw Nextcloud en dat de volgende applicatie geïnstalleerd is voordat u de installatie uitvoert:" type_link_text: "Integratie met OpenProject\"" name: "Geef uw opslag een naam zodat gebruikers onderscheid kunnen maken tussen meerdere opslagplaatsen." host: "Voeg het hostadres van je opslag toe, inclusief de https://. Het mag niet langer dan 255 tekens zijn." managed_project_folders_application_password: > - Copy this value from: + Kopieer deze waarde van: no_storage_set_up: "There are no file storages set up yet." no_specific_folder: "By default, each user will start at their own home folder when they upload a file." automatic_folder: "This will automatically create a root folder for this project and manage the access permissions for each project member." @@ -126,10 +127,10 @@ nl: label_existing_manual_folder: "Existing folder with manually managed permissions" label_no_specific_folder: "No specific folder" label_automatic_folder: "New folder with automatically managed permissions" - label_no_selected_folder: "No selected folder" + label_no_selected_folder: "Geen geselecteerde map" label_storage: "Opslag" label_storages: "Opslagplaatsen" - no_results: "No storages set up yet." + no_results: "Er zijn nog geen opslagplaatsen ingesteld." provider_types: label: "Type provider" nextcloud: diff --git a/modules/storages/config/locales/crowdin/no.yml b/modules/storages/config/locales/crowdin/no.yml index f6e49434d5d..09c5443b43b 100644 --- a/modules/storages/config/locales/crowdin/no.yml +++ b/modules/storages/config/locales/crowdin/no.yml @@ -16,7 +16,7 @@ not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/pl.yml b/modules/storages/config/locales/crowdin/pl.yml index c99d5f530a5..2809807588b 100644 --- a/modules/storages/config/locales/crowdin/pl.yml +++ b/modules/storages/config/locales/crowdin/pl.yml @@ -16,7 +16,7 @@ pl: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Magazyn" attributes: storages/storage: @@ -50,6 +50,7 @@ pl: errors: too_many_elements_created_at_once: "Zbyt wiele elementów utworzonych jednocześnie. Oczekiwano co najwyżej %{max} , otrzymano %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Gotowe. Kontynuuj konfigurację" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/pt.yml b/modules/storages/config/locales/crowdin/pt.yml index 74a1c1d7618..07982d5fcda 100644 --- a/modules/storages/config/locales/crowdin/pt.yml +++ b/modules/storages/config/locales/crowdin/pt.yml @@ -16,7 +16,7 @@ pt: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Armazenamento" attributes: storages/storage: @@ -50,6 +50,7 @@ pt: errors: too_many_elements_created_at_once: "Muitos elementos criados ao mesmo tempo. Esperado %{max} no máximo, obteve %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Concluído. Continuar configuração" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/ro.yml b/modules/storages/config/locales/crowdin/ro.yml index 82cf5e87982..c4b51282629 100644 --- a/modules/storages/config/locales/crowdin/ro.yml +++ b/modules/storages/config/locales/crowdin/ro.yml @@ -16,7 +16,7 @@ ro: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Depozitare" attributes: storages/storage: @@ -50,6 +50,7 @@ ro: errors: too_many_elements_created_at_once: "Prea multe elemente create simultan. Se așteaptă cel mult %{max} , a luat %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Terminat. Continuă configurarea" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/ru.yml b/modules/storages/config/locales/crowdin/ru.yml index 8236d2d0c12..a6ad543fc64 100644 --- a/modules/storages/config/locales/crowdin/ru.yml +++ b/modules/storages/config/locales/crowdin/ru.yml @@ -12,11 +12,11 @@ ru: errors: attributes: storage_error: - not_authorized: "Not authorized for external connection to storage." - not_found: "The requested resource could not be found at the external file storage." + not_authorized: "Не авторизован для внешнего подключения к хранилищу." + not_found: "Запрошенный ресурс во внешнем файловом хранилище найти не удалось." activerecord: models: - file_link: "Ссылка на файл" + file_link: "Файл" storages/storage: "Хранилище" attributes: storages/storage: @@ -50,6 +50,7 @@ ru: errors: too_many_elements_created_at_once: "Слишком много элементов, созданных сразу. Ожидалось %{max} максимум - получено %{actual}." storages: + unknown_storage: "Неизвестное хранилище" buttons: done_continue_setup: "Готово. Продолжить установку" done_complete_setup: "Готово, завершить установку" @@ -69,7 +70,7 @@ ru: index: "Файловые хранилища, доступные в этом проекте" new: "Добавить хранилище файлов в этот проект" edit: "Редактировать хранилище файлов для этого проекта" - delete: "Delete file storage" + delete: "Удалить хранилище файлов" instructions: type: "Перед установкой убедитесь, что у вас есть права администрирования в вашем экземпляре Nextcloud и что перед установкой установлено следующее приложение:" type_link_text: "«Интеграционный OpenProject»" @@ -99,11 +100,11 @@ ru: storage: > Вы уверены, что хотите удалить это хранилище? Это также удалит хранилище из всех проектов, где оно используется. Кроме того, будут удалены все ссылки из пакетов работ на файлы, хранящиеся в этом хранилище. project_storage: > - Are you sure you want to delete %{file_storage} from this project? To confirm this action please introduce the storage name in the field below, this will: - project_storage_delete_result_1: "Remove all links from work packages of this project to files and folders of that storage." - project_storage_delete_result_2: "In case this storage has an automatically managed project folder, this and its files will be deleted forever." - input_delete_confirmation: "Enter the file storage name %{file_storage} to confirm deletion." - irreversible_notice: "Deleting a file storage is an irreversible action." + Вы уверены, что хотите удалить %{file_storage} из этого проекта? Для подтверждения этого действия, пожалуйста, введите имя хранилища в поле ниже, это позволит: + project_storage_delete_result_1: "Удалить все ссылки из пакетов работ этого проекта в файлы и папки этого хранилища." + project_storage_delete_result_2: "В случае, если хранилище имеет автоматически управляемую папку проекта, она и ее файлы будут удалены навсегда." + input_delete_confirmation: "Введите имя хранилища файлов %{file_storage} для подтверждения удаления." + irreversible_notice: "Удаление хранилища файлов является необратимым действием." label_active: "Активный" label_inactive: "Неактивный" label_creator: "Создатель" diff --git a/modules/storages/config/locales/crowdin/rw.yml b/modules/storages/config/locales/crowdin/rw.yml index 06d2658cfd7..c3161516718 100644 --- a/modules/storages/config/locales/crowdin/rw.yml +++ b/modules/storages/config/locales/crowdin/rw.yml @@ -16,7 +16,7 @@ rw: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ rw: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/si.yml b/modules/storages/config/locales/crowdin/si.yml index 5f01d383059..c8ab555249c 100644 --- a/modules/storages/config/locales/crowdin/si.yml +++ b/modules/storages/config/locales/crowdin/si.yml @@ -16,7 +16,7 @@ si: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ si: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/sk.yml b/modules/storages/config/locales/crowdin/sk.yml index 070106fddff..f7dbf6da621 100644 --- a/modules/storages/config/locales/crowdin/sk.yml +++ b/modules/storages/config/locales/crowdin/sk.yml @@ -16,7 +16,7 @@ sk: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ sk: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/sl.yml b/modules/storages/config/locales/crowdin/sl.yml index ba2a0bfcafd..b24dd968916 100644 --- a/modules/storages/config/locales/crowdin/sl.yml +++ b/modules/storages/config/locales/crowdin/sl.yml @@ -16,7 +16,7 @@ sl: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ sl: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/sr.yml b/modules/storages/config/locales/crowdin/sr.yml index 1cb4664edd7..f05cbb1c09e 100644 --- a/modules/storages/config/locales/crowdin/sr.yml +++ b/modules/storages/config/locales/crowdin/sr.yml @@ -16,7 +16,7 @@ sr: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ sr: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/sv.yml b/modules/storages/config/locales/crowdin/sv.yml index 31130e80216..756f92b24cc 100644 --- a/modules/storages/config/locales/crowdin/sv.yml +++ b/modules/storages/config/locales/crowdin/sv.yml @@ -16,7 +16,7 @@ sv: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ sv: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/th.yml b/modules/storages/config/locales/crowdin/th.yml index f64ccc4fbf7..9067a2eed1e 100644 --- a/modules/storages/config/locales/crowdin/th.yml +++ b/modules/storages/config/locales/crowdin/th.yml @@ -16,7 +16,7 @@ th: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ th: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/tr.yml b/modules/storages/config/locales/crowdin/tr.yml index d4e14dd5f98..eaf266a0261 100644 --- a/modules/storages/config/locales/crowdin/tr.yml +++ b/modules/storages/config/locales/crowdin/tr.yml @@ -16,7 +16,7 @@ tr: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Depolama" attributes: storages/storage: @@ -50,6 +50,7 @@ tr: errors: too_many_elements_created_at_once: "Aynı anda çok fazla öğe oluşturuldu. En fazla %{max} bekleniyordu, %{actual} alındı." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Tamamlandı. Kuruluma devam et" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/uk.yml b/modules/storages/config/locales/crowdin/uk.yml index 68ad3b7337a..99ef34ab5d4 100644 --- a/modules/storages/config/locales/crowdin/uk.yml +++ b/modules/storages/config/locales/crowdin/uk.yml @@ -16,7 +16,7 @@ uk: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Сховище" attributes: storages/storage: @@ -50,6 +50,7 @@ uk: errors: too_many_elements_created_at_once: "Забагато елементів, створених за раз. Очікувалося щонайбільше %{max}; отримано %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Готово – продовжити налаштування" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/vi.yml b/modules/storages/config/locales/crowdin/vi.yml index 7209edd0d93..54112dee868 100644 --- a/modules/storages/config/locales/crowdin/vi.yml +++ b/modules/storages/config/locales/crowdin/vi.yml @@ -16,7 +16,7 @@ vi: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ vi: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/crowdin/zh-TW.yml b/modules/storages/config/locales/crowdin/zh-TW.yml index 921a4f41a73..974e626878d 100644 --- a/modules/storages/config/locales/crowdin/zh-TW.yml +++ b/modules/storages/config/locales/crowdin/zh-TW.yml @@ -16,7 +16,7 @@ zh-TW: not_found: "The requested resource could not be found at the external file storage." activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -50,6 +50,7 @@ zh-TW: errors: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/en.yml b/modules/storages/config/locales/en.yml index 8dc1ffa8bd5..3083831789a 100644 --- a/modules/storages/config/locales/en.yml +++ b/modules/storages/config/locales/en.yml @@ -18,7 +18,7 @@ en: activerecord: models: - file_link: "File link" + file_link: "File" storages/storage: "Storage" attributes: storages/storage: @@ -58,6 +58,7 @@ en: too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." storages: + unknown_storage: "Unknown storage" buttons: done_continue_setup: "Done. Continue setup" done_complete_setup: "Done, complete setup" diff --git a/modules/storages/config/locales/js-en.yml b/modules/storages/config/locales/js-en.yml index f5bdb891a1a..275104d81a9 100644 --- a/modules/storages/config/locales/js-en.yml +++ b/modules/storages/config/locales/js-en.yml @@ -36,6 +36,7 @@ en: to upload this file. What would you like to do? directory_not_writeable: "You do not have permission to add files to this folder." dragging_many_files: "The upload to %{storageType} supports only one file at once." + dragging_folder: "The upload to %{storageType} does not support folders." empty_folder: "This folder is empty." empty_folder_location_hint: "Click the button below to upload the file to this location." file_not_selectable_location: "Selecting a file is not possible in the process of choosing a location." diff --git a/modules/storages/config/routes.rb b/modules/storages/config/routes.rb index d8215473550..38a808fc750 100644 --- a/modules/storages/config/routes.rb +++ b/modules/storages/config/routes.rb @@ -43,7 +43,7 @@ OpenProject::Application.routes.draw do scope 'projects/:project_id', as: 'project' do namespace 'settings' do - resources :projects_storages, controller: '/storages/admin/projects_storages', except: %i[show] do + resources :project_storages, controller: '/storages/admin/project_storages', except: %i[show] do member do # Destroy uses a get request to prompt the user before the actual DELETE request get :destroy_info, as: 'confirm_destroy' diff --git a/modules/storages/db/migrate/20230802085026_rename_projects_storages_table.rb b/modules/storages/db/migrate/20230802085026_rename_projects_storages_table.rb new file mode 100644 index 00000000000..f05cee4887a --- /dev/null +++ b/modules/storages/db/migrate/20230802085026_rename_projects_storages_table.rb @@ -0,0 +1,34 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +class RenameProjectsStoragesTable < ActiveRecord::Migration[7.0] + def change + rename_table :projects_storages, :project_storages + rename_column :last_project_folders, :projects_storage_id, :project_storage_id + end +end diff --git a/modules/storages/lib/api/v3/storage_files/storage_files_api.rb b/modules/storages/lib/api/v3/storage_files/storage_files_api.rb index 15ad3567a69..8cb356ff285 100644 --- a/modules/storages/lib/api/v3/storage_files/storage_files_api.rb +++ b/modules/storages/lib/api/v3/storage_files/storage_files_api.rb @@ -29,7 +29,7 @@ module API::V3::StorageFiles class StorageFilesAPI < ::API::OpenProjectAPI using Storages::Peripherals::ServiceResultRefinements - helpers Storages::Peripherals::StorageErrorHelper + helpers Storages::Peripherals::StorageErrorHelper, Storages::Peripherals::StorageFileInfoConverter resources :files do get do @@ -47,8 +47,9 @@ module API::V3::StorageFiles get do Storages::Peripherals::StorageRequests .new(storage: @storage) - .file_query - .call(user: current_user, file_id: params[:file_id]) + .files_info_query + .call(user: current_user, file_ids: [params[:file_id]]).map(&:first) + .map { |file_info| to_storage_file(file_info) } .match( on_success: ->(storage_file) { API::V3::StorageFiles::StorageFileRepresenter.new(storage_file, @storage, current_user:) diff --git a/modules/storages/lib/open_project/storages/engine.rb b/modules/storages/lib/open_project/storages/engine.rb index bdb6bbfbdeb..6932a78c3ec 100644 --- a/modules/storages/lib/open_project/storages/engine.rb +++ b/modules/storages/lib/open_project/storages/engine.rb @@ -32,6 +32,9 @@ # gets loaded. module OpenProject::Storages class Engine < ::Rails::Engine + def self.permissions + @permissions ||= Storages::GroupFolderPropertiesSyncService::PERMISSIONS_MAP.keys + end # engine name is used as a default prefix for module tables when generating # tables with the rails command. # It may also be used in other places, please investigate. @@ -42,23 +45,51 @@ module OpenProject::Storages initializer 'openproject_storages.feature_decisions' do OpenProject::FeatureDecisions.add :storage_file_picking_select_all - OpenProject::FeatureDecisions.add :storage_project_folders - OpenProject::FeatureDecisions.add :managed_project_folders - OpenProject::FeatureDecisions.add :automatically_managed_project_folders end initializer 'openproject_storages.event_subscriptions' do Rails.application.config.after_initialize do - if OpenProject::FeatureDecisions.managed_project_folders_active? - [ - OpenProject::Events::MEMBER_CREATED, - OpenProject::Events::MEMBER_UPDATED, - OpenProject::Events::MEMBER_DESTROYED, - OpenProject::Events::PROJECT_CREATED, - OpenProject::Events::PROJECT_UPDATED, - OpenProject::Events::PROJECT_RENAMED - ].each do |event| - OpenProject::Notifications.subscribe(event) do |_payload| + [ + OpenProject::Events::MEMBER_CREATED, + OpenProject::Events::MEMBER_UPDATED, + OpenProject::Events::MEMBER_DESTROYED, + OpenProject::Events::PROJECT_UPDATED, + OpenProject::Events::PROJECT_RENAMED + ].each do |event| + OpenProject::Notifications.subscribe(event) do |_payload| + ::Storages::ManageNextcloudIntegrationEventsJob.debounce + end + end + + OpenProject::Notifications.subscribe( + OpenProject::Events::OAUTH_CLIENT_TOKEN_CREATED + ) do |payload| + if payload[:integration_type] == 'Storages::Storage' + ::Storages::ManageNextcloudIntegrationEventsJob.debounce + end + end + OpenProject::Notifications.subscribe( + OpenProject::Events::ROLE_UPDATED + ) do |payload| + if payload[:permissions_diff]&.intersect?(OpenProject::Storages::Engine.permissions) + ::Storages::ManageNextcloudIntegrationEventsJob.debounce + end + end + OpenProject::Notifications.subscribe( + OpenProject::Events::ROLE_DESTROYED + ) do |payload| + if payload[:permissions]&.intersect?(OpenProject::Storages::Engine.permissions) + ::Storages::ManageNextcloudIntegrationEventsJob.debounce + end + end + + [ + OpenProject::Events::PROJECT_STORAGE_CREATED, + OpenProject::Events::PROJECT_STORAGE_UPDATED, + OpenProject::Events::PROJECT_STORAGE_DESTROYED + ].each do |event| + OpenProject::Notifications.subscribe(event) do |payload| + if payload[:project_folder_mode] == :automatic ::Storages::ManageNextcloudIntegrationEventsJob.debounce end end @@ -87,27 +118,11 @@ module OpenProject::Storages dependencies: %i[view_file_links], contract_actions: { file_links: %i[manage] } permission :manage_storages_in_project, - { 'storages/admin/projects_storages': %i[index new edit update create destroy destroy_info set_permissions] }, + { 'storages/admin/project_storages': %i[index new edit update create destroy destroy_info set_permissions] }, dependencies: %i[] - # explicit check for test env is needed, because `with_flag: { managed_project_folders: true }` set for a test case - # handled later and at this moment feature is disabled. - if OpenProject::FeatureDecisions.managed_project_folders_active? || Rails.env.test? - permission :read_files, - {}, - dependencies: %i[] - permission :write_files, - {}, - dependencies: %i[] - permission :create_files, - {}, - dependencies: %i[] - permission :delete_files, - {}, - dependencies: %i[] - permission :share_files, - {}, - dependencies: %i[] + OpenProject::Storages::Engine.permissions.each do |p| + permission(p, {}, dependencies: %i[]) end end @@ -122,17 +137,17 @@ module OpenProject::Storages icon: 'hosting' menu :project_menu, - :settings_projects_storages, - { controller: '/storages/admin/projects_storages', action: 'index' }, + :settings_project_storages, + { controller: '/storages/admin/project_storages', action: 'index' }, caption: :project_module_storages, parent: :settings configure_menu :project_menu do |menu, project| if project.present? && - User.current.logged? && - User.current.member_of?(project) && - User.current.allowed_to?(:view_file_links, project) - project.projects_storages.each do |project_storage| + User.current.logged? && + User.current.member_of?(project) && + User.current.allowed_to?(:view_file_links, project) + project.project_storages.each do |project_storage| storage = project_storage.storage href = if project_storage.project_folder_inactive? storage.host @@ -248,12 +263,9 @@ module OpenProject::Storages add_cron_jobs do [ - Storages::CleanupUncontaineredFileLinksJob - ].tap do |cron_jobs| - if OpenProject::FeatureDecisions.managed_project_folders_active? - cron_jobs << Storages::ManageNextcloudIntegrationCronJob - end - end + Storages::CleanupUncontaineredFileLinksJob, + Storages::ManageNextcloudIntegrationCronJob + ] end end end diff --git a/modules/storages/spec/common/peripherals/files_info_query_spec.rb b/modules/storages/spec/common/peripherals/files_info_query_spec.rb new file mode 100644 index 00000000000..21fa894e433 --- /dev/null +++ b/modules/storages/spec/common/peripherals/files_info_query_spec.rb @@ -0,0 +1,178 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' + +RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::FilesInfoQuery, webmock: true do + using Storages::Peripherals::ServiceResultRefinements + + let(:user) { create(:user) } + let(:url) { 'https://example.com' } + let(:origin_user_id) { 'admin' } + let(:storage) { build(:nextcloud_storage, :as_not_automatically_managed, host: url) } + + let(:oauth_client) { create(:oauth_client, integration: storage) } + let(:token) { create(:oauth_client_token, origin_user_id:, access_token: 'xyz', oauth_client:, user:) } + + before { token } + + subject { described_class.new(storage) } + + describe '#call' do + let(:file_ids) { %w[354 355] } + + context 'without outbound request involved' do + context 'with an empty array of file ids' do + it 'returns an empty array' do + result = subject.call(user:, file_ids: []) + + expect(result).to be_success + expect(result.result).to eq([]) + end + end + + context 'with nil' do + it 'returns an error' do + result = subject.call(user:, file_ids: nil) + + expect(result).to be_failure + expect(result.result).to eq(:error) + end + end + end + + context 'with outbound request successful' do + let(:expected_response_body) do + <<~JSON + { + "ocs": { + "meta": { + "status": "ok", + "statuscode": 100, + "message": "OK", + "totalitems": "", + "itemsperpage": "" + }, + "data": { + "354": { + "status": "OK", + "statuscode": 200, + "id": 354, + "name": "Demo project (1)", + "mtime": 1689162221, + "ctime": 0, + "mimetype": "application/x-op-directory", + "size": 989752, + "owner_name": "admin", + "owner_id": "admin", + "trashed": false, + "modifier_name": null, + "modifier_id": null, + "dav_permissions": "RMGDNVCK", + "path": "files/OpenProject/Demo project (1)" + }, + "355": { + "status": "OK", + "statuscode": 200, + "id": 355, + "name": "minecraft.jpg", + "mtime": 1689162221, + "ctime": 0, + "mimetype": "image/jpeg", + "size": 989752, + "owner_name": "admin", + "owner_id": "admin", + "trashed": false, + "modifier_name": null, + "modifier_id": null, + "dav_permissions": "RMGDNVW", + "path": "files/OpenProject/Demo project (1)/minecraft.jpg" + } + } + } + } + JSON + end + + before do + stub_request(:post, "https://example.com/ocs/v1.php/apps/integration_openproject/filesinfo") + .with(body: { fileIds: file_ids }.to_json) + .to_return(status: 200, body: expected_response_body) + end + + context 'with an array of file ids' do + it 'must return an array of file information when called' do + result = subject.call(user:, file_ids:) + expect(result).to be_success + + result.match( + on_success: ->(file_infos) do + expect(file_infos.size).to eq(2) + expect(file_infos).to all(be_a(Storages::StorageFileInfo)) + end, + on_failure: ->(error) { fail "Expected success, got #{error}" } + ) + end + end + end + + context 'with outbound request not authorized' do + before do + stub_request(:post, "https://example.com/ocs/v1.php/apps/integration_openproject/filesinfo") + .with(body: { fileIds: file_ids }.to_json) + .to_return(status: 401) + end + + context 'with an array of file ids' do + it 'must return an error when called' do + subject.call(user:, file_ids:).match( + on_success: ->(file_infos) { fail "Expected failure, got #{file_infos}" }, + on_failure: ->(error) { expect(error.code).to eq(:not_authorized) } + ) + end + end + end + + context 'with outbound request not found' do + before do + stub_request(:post, "https://example.com/ocs/v1.php/apps/integration_openproject/filesinfo") + .with(body: { fileIds: file_ids }.to_json) + .to_return(status: 404) + end + + context 'with an array of file ids' do + it 'must return an error when called' do + subject.call(user:, file_ids:).match( + on_success: ->(file_infos) { fail "Expected failure, got #{file_infos}" }, + on_failure: ->(error) { expect(error.code).to eq(:not_found) } + ) + end + end + end + end +end diff --git a/modules/storages/spec/common/peripherals/storage_requests_spec.rb b/modules/storages/spec/common/peripherals/storage_requests_spec.rb index 861efc53f7f..513894f389f 100644 --- a/modules/storages/spec/common/peripherals/storage_requests_spec.rb +++ b/modules/storages/spec/common/peripherals/storage_requests_spec.rb @@ -310,66 +310,6 @@ RSpec.describe Storages::Peripherals::StorageRequests, webmock: true do include_examples 'outbound is failing', 500, :error end - describe '#file_query' do - let(:file_id) { '819' } - let(:expected_response_body) do - <<~JSON - { - "ocs": { - "meta": { - "status": "ok", - "statuscode": 100, - "message": "OK", - "totalitems": "", - "itemsperpage": "" - }, - "data": { - "status": "OK", - "statuscode": 200, - "id": 819, - "name": "[Sample] Project Name | Ehuuu(10)", - "mtime": 1684491252, - "ctime": 0, - "mimetype": "application\\/x-op-directory", - "size": 0, - "owner_name": "OpenProject", - "owner_id": "OpenProject", - "trashed": false, - "modifier_name": null, - "modifier_id": null, - "dav_permissions": "RMGDNVCK", - "path": "files\\/OpenProject\\/[Sample] Project Name | Ehuuu(10)" - } - } - } - JSON - end - - before do - stub_request(:get, "https://example.com/ocs/v1.php/apps/integration_openproject/fileinfo/#{file_id}") - .with(headers: { 'Accept' => 'application/json', - 'Authorization' => 'Bearer xyz', - 'Content-Type' => 'application/json' }) - .to_return(status: 200, body: expected_response_body, headers: {}) - end - - context 'with Nextcloud storage type selected' do - it 'must return a list of files when called' do - result = subject - .file_query - .call(user:, file_id:) - expect(result).to be_success - storage_file = result.result - expect(storage_file.id).to eq(819) - expect(storage_file.location).to eq("/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuuu%2810%29") - expect(storage_file.mime_type).to eq("application/x-op-directory") - expect(storage_file.name).to eq("[Sample] Project Name | Ehuuu(10)") - expect(storage_file.permissions).to eq("RMGDNVCK") - expect(storage_file.size).to eq(0) - end - end - end - describe '#upload_link_query' do let(:query_payload) { Struct.new(:parent).new(42) } let(:upload_token) { 'valid-token' } @@ -857,7 +797,7 @@ RSpec.describe Storages::Peripherals::StorageRequests, webmock: true do end end - describe '#propfind_query' do + describe '#file_ids_query' do let(:nextcloud_subpath) { '' } let(:url) { "https://example.com#{nextcloud_subpath}" } let(:expected_request_body) do @@ -955,11 +895,11 @@ RSpec.describe Storages::Peripherals::StorageRequests, webmock: true do ).to_return(status: 200, body: expected_response_body, headers: {}) end - shared_examples 'a propfind_query response' do + shared_examples 'a file_ids_query response' do it 'responds with a list of paths and attributes for each of them' do result = subject - .propfind_query - .call(depth: '1', path: 'OpenProject', props: %w[oc:fileid]) + .file_ids_query + .call(path: 'OpenProject') .result expect(result).to eq({ "OpenProject/" => { "fileid" => "349" }, "OpenProject/Project #2/" => { "fileid" => "381" }, @@ -971,12 +911,12 @@ RSpec.describe Storages::Peripherals::StorageRequests, webmock: true do end end - it_behaves_like 'a propfind_query response' + it_behaves_like 'a file_ids_query response' context 'when NC is deployed under subpath' do let(:nexcloud_subpath) { '/subpath' } - it_behaves_like 'a propfind_query response' + it_behaves_like 'a file_ids_query response' end end diff --git a/modules/storages/spec/contracts/storages/last_project_folders/base_contract_spec.rb b/modules/storages/spec/contracts/storages/last_project_folders/base_contract_spec.rb index adcd8f07f1d..4b78543b4b2 100644 --- a/modules/storages/spec/contracts/storages/last_project_folders/base_contract_spec.rb +++ b/modules/storages/spec/contracts/storages/last_project_folders/base_contract_spec.rb @@ -37,7 +37,7 @@ RSpec.describe Storages::LastProjectFolders::BaseContract do context 'if no project storage is given' do before do - last_project_folder.projects_storage = nil + last_project_folder.project_storage = nil end it_behaves_like 'contract is invalid' diff --git a/modules/storages/spec/factories/last_project_folder_factory.rb b/modules/storages/spec/factories/last_project_folder_factory.rb index e229c4603da..19eeaec7de8 100644 --- a/modules/storages/spec/factories/last_project_folder_factory.rb +++ b/modules/storages/spec/factories/last_project_folder_factory.rb @@ -28,6 +28,6 @@ FactoryBot.define do factory :last_project_folder, class: '::Storages::LastProjectFolder' do - projects_storage factory: :project_storage + project_storage factory: :project_storage end end diff --git a/modules/storages/spec/factories/storage_file_info_factory.rb b/modules/storages/spec/factories/storage_file_info_factory.rb new file mode 100644 index 00000000000..1dd9f06bbad --- /dev/null +++ b/modules/storages/spec/factories/storage_file_info_factory.rb @@ -0,0 +1,52 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +FactoryBot.define do + factory :storage_file_info, class: '::Storages::StorageFileInfo' do + status { "OK" } + status_code { 200 } + sequence(:id) { |n| "20000#{n}" } + sequence(:name) { |n| "file_name_#{n}.txt" } + last_modified_at { Time.zone.now } + created_at { Time.zone.now } + mime_type { "text/plain" } + sequence(:size) { |n| n * 123 } + owner_name { "Peter Pan" } + owner_id { "peter" } + trashed { false } + last_modified_by_name { "Petra Panadera" } + last_modified_by_id { "petra" } + permissions { "RMGDNVCK" } + sequence(:location) { |n| "files/peter/file_name_#{n}.txt" } + + initialize_with do + new(status, status_code, id, name, last_modified_at, created_at, mime_type, size, owner_name, owner_id, + trashed, last_modified_by_name, last_modified_by_id, permissions, location) + end + end +end diff --git a/modules/storages/spec/features/admin_storages_spec.rb b/modules/storages/spec/features/admin_storages_spec.rb index d86e58ce7f1..8b80e566417 100644 --- a/modules/storages/spec/features/admin_storages_spec.rb +++ b/modules/storages/spec/features/admin_storages_spec.rb @@ -28,7 +28,7 @@ require_relative '../spec_helper' -RSpec.describe 'Admin storages', :storage_server_helpers, js: true, with_flag: { automatically_managed_project_folders: true } do +RSpec.describe 'Admin storages', :storage_server_helpers, js: true do let(:admin) { create(:admin) } before do diff --git a/modules/storages/spec/features/delete_project_storage_and_file_links_spec.rb b/modules/storages/spec/features/delete_project_storage_and_file_links_spec.rb index 829d436fd60..8f49cc9ae06 100644 --- a/modules/storages/spec/features/delete_project_storage_and_file_links_spec.rb +++ b/modules/storages/spec/features/delete_project_storage_and_file_links_spec.rb @@ -63,7 +63,7 @@ RSpec.describe 'Delete ProjectStorage with FileLinks', js: true, webmock: true d it 'deletes ProjectStorage with dependent FileLinks' do # Go to Projects -> Settings -> File Storages - visit project_settings_projects_storages_path(project) + visit project_settings_project_storages_path(project) # The list of enabled file storages should now contain Storage 1 expect(page).to have_text('File storages available in this project') @@ -79,7 +79,7 @@ RSpec.describe 'Delete ProjectStorage with FileLinks', js: true, webmock: true d # Cancel Confirmation page.click_link('Cancel') - expect(page).to have_current_path project_settings_projects_storages_path(project) + expect(page).to have_current_path project_settings_project_storages_path(project) page.find('.icon.icon-delete').click @@ -88,7 +88,7 @@ RSpec.describe 'Delete ProjectStorage with FileLinks', js: true, webmock: true d page.click_button('Delete') # List of ProjectStorages empty again - expect(page).to have_current_path project_settings_projects_storages_path(project) + expect(page).to have_current_path project_settings_project_storages_path(project) expect(page).to have_text(I18n.t('storages.no_results')) # Also check in the database that ProjectStorage and dependent FileLinks are gone diff --git a/modules/storages/spec/features/manage_project_storage_spec.rb b/modules/storages/spec/features/manage_project_storage_spec.rb index f79cc9533bb..34b8a91783a 100644 --- a/modules/storages/spec/features/manage_project_storage_spec.rb +++ b/modules/storages/spec/features/manage_project_storage_spec.rb @@ -34,9 +34,7 @@ require_relative '../spec_helper' RSpec.describe( 'Activation of storages in projects', js: true, - webmock: true, - with_flag: { storage_project_folders: true, - managed_project_folders: true } + webmock: true ) do let(:user) { create(:user) } # The first page is the Project -> Settings -> General page, so we need @@ -66,14 +64,19 @@ RSpec.describe( let(:folder1_fileinfo_response) do { ocs: { + meta: { + status: 'ok' + }, data: { - status: 'OK', - statuscode: 200, - id: 11, - name: 'Folder1', - path: 'files/Folder1', - mtime: 1682509719, - ctime: 0 + '11': { + status: 'OK', + statuscode: 200, + id: 11, + name: 'Folder1', + path: 'files/Folder1', + mtime: 1682509719, + ctime: 0 + } } } } @@ -86,7 +89,7 @@ RSpec.describe( .to_return(status: 207, body: root_xml_response, headers: {}) stub_request(:propfind, "#{storage.host}/remote.php/dav/files/#{oauth_client_token.origin_user_id}/Folder1") .to_return(status: 207, body: folder1_xml_response, headers: {}) - stub_request(:get, "#{storage.host}/ocs/v1.php/apps/integration_openproject/fileinfo/11") + stub_request(:post, "#{storage.host}/ocs/v1.php/apps/integration_openproject/filesinfo") .to_return(status: 200, body: folder1_fileinfo_response.to_json, headers: {}) stub_request(:get, "#{storage.host}/ocs/v1.php/cloud/user").to_return(status: 200, body: "{}") stub_request( @@ -106,19 +109,19 @@ RSpec.describe( # Check for an empty table in Project -> Settings -> File storages expect(page).to have_title('File storages') - expect(page).to have_current_path project_settings_projects_storages_path(project) + expect(page).to have_current_path project_settings_project_storages_path(project) expect(page).to have_text(I18n.t('storages.no_results')) page.find('.toolbar .button--icon.icon-add').click # Can cancel the creation of a new file storage - expect(page).to have_current_path new_project_settings_projects_storage_path(project_id: project) + expect(page).to have_current_path new_project_settings_project_storage_path(project_id: project) expect(page).to have_text('Add a file storage') page.click_link('Cancel') - expect(page).to have_current_path project_settings_projects_storages_path(project) + expect(page).to have_current_path project_settings_project_storages_path(project) # Enable one file storage together with a project folder mode page.find('.toolbar .button--icon.icon-add').click - expect(page).to have_current_path new_project_settings_projects_storage_path(project_id: project) + expect(page).to have_current_path new_project_settings_project_storage_path(project_id: project) expect(page).to have_text('Add a file storage') expect(page).to have_select('storages_project_storage_storage_id', options: ["#{storage.name} (#{storage.short_provider_type})"]) @@ -146,15 +149,15 @@ RSpec.describe( # The list of enabled file storages should now contain Storage 1 expect(page).to have_text('File storages available in this project') - expect(page).to have_text('Storage 1') + expect(page).to have_text(storage.name) # Press Edit icon to change the project folder mode to inactive page.find('.icon.icon-edit').click - expect(page).to have_current_path edit_project_settings_projects_storage_path(project_id: project, + expect(page).to have_current_path edit_project_settings_project_storage_path(project_id: project, id: Storages::ProjectStorage.last) expect(page).to have_text('Edit the file storage to this project') expect(page).not_to have_select('storages_project_storage_storage_id') - expect(page).to have_text('Storage 1') + expect(page).to have_text(storage.name) expect(page).to have_checked_field('storages_project_storage_project_folder_mode_manual') expect(page).to have_text('Folder1') @@ -165,15 +168,15 @@ RSpec.describe( # The list of enabled file storages should still contain Storage 1 expect(page).to have_text('File storages available in this project') - expect(page).to have_text('Storage 1') + expect(page).to have_text(storage.name) # Click Edit icon again but cancel the edit page.find('.icon.icon-edit').click - expect(page).to have_current_path edit_project_settings_projects_storage_path(project_id: project, + expect(page).to have_current_path edit_project_settings_project_storage_path(project_id: project, id: Storages::ProjectStorage.last) expect(page).to have_text('Edit the file storage to this project') page.click_link('Cancel') - expect(page).to have_current_path project_settings_projects_storages_path(project) + expect(page).to have_current_path project_settings_project_storages_path(project) # Press Delete icon to remove the storage from the project page.find('.icon.icon-delete').click @@ -185,16 +188,16 @@ RSpec.describe( # Cancel Confirmation page.click_link('Cancel') - expect(page).to have_current_path project_settings_projects_storages_path(project) + expect(page).to have_current_path project_settings_project_storages_path(project) page.find('.icon.icon-delete').click # Approve Confirmation - page.fill_in 'delete_confirmation', with: "Storage 1" + page.fill_in 'delete_confirmation', with: storage.name page.click_button('Delete') # List of ProjectStorages empty again - expect(page).to have_current_path project_settings_projects_storages_path(project) + expect(page).to have_current_path project_settings_project_storages_path(project) expect(page).to have_text(I18n.t('storages.no_results')) end end diff --git a/modules/storages/spec/features/storages_module_spec.rb b/modules/storages/spec/features/storages_module_spec.rb index c70693d1c1a..2a60d6edc54 100644 --- a/modules/storages/spec/features/storages_module_spec.rb +++ b/modules/storages/spec/features/storages_module_spec.rb @@ -118,7 +118,7 @@ RSpec.describe 'Storages module', js: true do context 'when showing project storages settings page' do context 'with storages module is enabled' do before do - visit project_settings_projects_storages_path(project) + visit project_settings_project_storages_path(project) end it 'must show the page' do @@ -130,7 +130,7 @@ RSpec.describe 'Storages module', js: true do let(:project) { create(:project, enabled_module_names: %i[work_package_tracking]) } before do - visit project_settings_projects_storages_path(project) + visit project_settings_project_storages_path(project) end it 'mustn\'t show the page' do diff --git a/modules/storages/spec/lib/api/v3/storage_files/storage_file_representer_spec.rb b/modules/storages/spec/lib/api/v3/storage_files/storage_file_representer_spec.rb index ed40ac1fdd4..ef6d0496724 100644 --- a/modules/storages/spec/lib/api/v3/storage_files/storage_file_representer_spec.rb +++ b/modules/storages/spec/lib/api/v3/storage_files/storage_file_representer_spec.rb @@ -35,16 +35,16 @@ RSpec.describe API::V3::StorageFiles::StorageFileRepresenter do let(:storage) { build_stubbed(:storage) } let(:file) do Storages::StorageFile.new( - 42, - 'readme.md', - 4096, - 'text/plain', - created_at, - last_modified_at, - 'admin', - 'admin', - '/readme.md', - %i[readable writeable] + id: 42, + name: 'readme.md', + size: 4096, + mime_type: 'text/plain', + created_at:, + last_modified_at:, + created_by_name: 'admin', + last_modified_by_name: 'admin', + location: '/readme.md', + permissions: %i[readable writeable] ) end let(:representer) { described_class.new(file, storage, current_user: user) } diff --git a/modules/storages/spec/lib/api/v3/storage_files/storage_files_representer_spec.rb b/modules/storages/spec/lib/api/v3/storage_files/storage_files_representer_spec.rb index a30cbd48dda..07eded1df96 100644 --- a/modules/storages/spec/lib/api/v3/storage_files/storage_files_representer_spec.rb +++ b/modules/storages/spec/lib/api/v3/storage_files/storage_files_representer_spec.rb @@ -36,46 +36,46 @@ RSpec.describe API::V3::StorageFiles::StorageFilesRepresenter do let(:parent) do Storages::StorageFile.new( - 23, - 'Documents', - 2048, - 'application/x-op-directory', - created_at, - last_modified_at, - 'admin', - 'admin', - '/Documents', - %i[readable writeable] + id: 23, + name: 'Documents', + size: 2048, + mime_type: 'application/x-op-directory', + created_at:, + last_modified_at:, + created_by_name: 'admin', + last_modified_by_name: 'admin', + location: '/Documents', + permissions: %i[readable writeable] ) end let(:file) do Storages::StorageFile.new( - 42, - 'readme.md', - 4096, - 'text/plain', - created_at, - last_modified_at, - 'admin', - 'admin', - '/Documents/readme.md', - %i[readable writeable] + id: 42, + name: 'readme.md', + size: 4096, + mime_type: 'text/plain', + created_at:, + last_modified_at:, + created_by_name: 'admin', + last_modified_by_name: 'admin', + location: '/Documents/readme.md', + permissions: %i[readable writeable] ) end let(:ancestor) do Storages::StorageFile.new( - 47, - '/', - 4096, - 'application/x-op-directory', - created_at, - last_modified_at, - 'admin', - 'admin', - '/', - %i[readable writeable] + id: 47, + name: '/', + size: 4096, + mime_type: 'application/x-op-directory', + created_at:, + last_modified_at:, + created_by_name: 'admin', + last_modified_by_name: 'admin', + location: '/', + permissions: %i[readable writeable] ) end diff --git a/modules/storages/spec/permissions/manage_storage_in_project_spec.rb b/modules/storages/spec/permissions/manage_storage_in_project_spec.rb index 7cbc3114ade..1e50a29edd8 100644 --- a/modules/storages/spec/permissions/manage_storage_in_project_spec.rb +++ b/modules/storages/spec/permissions/manage_storage_in_project_spec.rb @@ -31,7 +31,7 @@ require 'support/permission_specs' require_module_spec_helper # rubocop:disable RSpec/EmptyExampleGroup -RSpec.describe Storages::Admin::ProjectsStoragesController, 'manage_storage_in_project permission', type: :controller do +RSpec.describe Storages::Admin::ProjectStoragesController, 'manage_storage_in_project permission', type: :controller do include PermissionSpecs controller_actions.each do |action| diff --git a/modules/storages/spec/requests/api/v3/file_links/mixed_case_file_links_integration_spec.rb b/modules/storages/spec/requests/api/v3/file_links/mixed_case_file_links_integration_spec.rb index 20cfad600fa..3f7ce6ef406 100644 --- a/modules/storages/spec/requests/api/v3/file_links/mixed_case_file_links_integration_spec.rb +++ b/modules/storages/spec/requests/api/v3/file_links/mixed_case_file_links_integration_spec.rb @@ -99,7 +99,8 @@ RSpec.describe 'API v3 file links resource' do size: 12706214, owner_id: "admin", owner_name: "admin", - trashed: false + trashed: false, + path: "/Nextcloud Manual.pdf" } end let(:file_info_trashed) do @@ -114,7 +115,8 @@ RSpec.describe 'API v3 file links resource' do size: 954123, owner_id: "admin", owner_name: "admin", - trashed: true + trashed: true, + path: "/Nextcloud Manual.pdf" } end diff --git a/modules/storages/spec/requests/api/v3/storages/storage_files_spec.rb b/modules/storages/spec/requests/api/v3/storages/storage_files_spec.rb index 502de4f257e..4f5012823a6 100644 --- a/modules/storages/spec/requests/api/v3/storages/storage_files_spec.rb +++ b/modules/storages/spec/requests/api/v3/storages/storage_files_spec.rb @@ -65,13 +65,43 @@ RSpec.describe 'API v3 storage files', content_type: :json, webmock: true do let(:response) do Storages::StorageFiles.new( [ - Storages::StorageFile.new(1, 'new_younglings.md', 4096, 'text/markdown', DateTime.now, DateTime.now, - 'Obi-Wan Kenobi', 'Obi-Wan Kenobi', '/', %i[readable]), - Storages::StorageFile.new(2, 'holocron_inventory.md', 4096, 'text/markdown', DateTime.now, DateTime.now, - 'Obi-Wan Kenobi', 'Obi-Wan Kenobi', '/', %i[readable writeable]) + Storages::StorageFile.new( + id: 1, + name: 'new_younglings.md', + size: 4096, + mime_type: 'text/markdown', + created_at: DateTime.now, + last_modified_at: DateTime.now, + created_by_name: 'Obi-Wan Kenobi', + last_modified_by_name: 'Obi-Wan Kenobi', + location: '/', + permissions: %i[readable] + ), + Storages::StorageFile.new( + id: 2, + name: 'holocron_inventory.md', + size: 4096, + mime_type: 'text/markdown', + created_at: DateTime.now, + last_modified_at: DateTime.now, + created_by_name: 'Obi-Wan Kenobi', + last_modified_by_name: 'Obi-Wan Kenobi', + location: '/', + permissions: %i[readable writeable] + ) ], - Storages::StorageFile.new(32, '/', 4096 * 2, 'application/x-op-directory', DateTime.now, DateTime.now, - 'Obi-Wan Kenobi', 'Obi-Wan Kenobi', '/', %i[readable writeable]), + Storages::StorageFile.new( + id: 32, + name: '/', + size: 4096 * 2, + mime_type: 'application/x-op-directory', + created_at: DateTime.now, + last_modified_at: DateTime.now, + created_by_name: 'Obi-Wan Kenobi', + last_modified_by_name: 'Obi-Wan Kenobi', + location: '/', + permissions: %i[readable writeable] + ), [] ) end diff --git a/modules/storages/spec/requests/api/v3/storages/storages_spec.rb b/modules/storages/spec/requests/api/v3/storages/storages_spec.rb index 5df88170d56..ec056bbf2a9 100644 --- a/modules/storages/spec/requests/api/v3/storages/storages_spec.rb +++ b/modules/storages/spec/requests/api/v3/storages/storages_spec.rb @@ -41,7 +41,7 @@ RSpec.describe 'API v3 storages resource', content_type: :json, webmock: true do end let(:oauth_application) { create(:oauth_application) } - let(:storage) { create(:storage, creator: current_user, oauth_application:) } + let(:storage) { create(:nextcloud_storage, creator: current_user, oauth_application:) } let(:project_storage) { create(:project_storage, project:, storage:) } let(:authorize_url) { 'https://example.com/authorize' } @@ -333,11 +333,21 @@ RSpec.describe 'API v3 storages resource', content_type: :json, webmock: true do describe 'DELETE /api/v3/storages/:storage_id' do let(:path) { api_v3_paths.storage(storage.id) } + let(:delete_folder_url) do + "#{storage.host}/remote.php/dav/files/#{storage.username}/#{project_storage.project_folder_path.chop}" + end + let(:deletion_request_stub) do + stub_request(:delete, delete_folder_url).to_return(status: 204, body: nil, headers: {}) + end subject(:last_response) do delete path end + before do + deletion_request_stub + end + context 'as admin' do let(:current_user) { create(:admin) } @@ -347,6 +357,10 @@ RSpec.describe 'API v3 storages resource', content_type: :json, webmock: true do context 'as non-admin' do context 'if user belongs to a project using the given storage' do it_behaves_like 'unauthorized access' + + it 'does not request project folder deletion' do + expect(deletion_request_stub).not_to have_been_requested + end end context 'if user does not belong to a project using the given storage' do @@ -355,6 +369,10 @@ RSpec.describe 'API v3 storages resource', content_type: :json, webmock: true do end it_behaves_like 'not found' + + it 'does not request project folder deletion' do + expect(deletion_request_stub).not_to have_been_requested + end end end end diff --git a/modules/storages/spec/services/storages/file_links/file_link_sync_service_spec.rb b/modules/storages/spec/services/storages/file_links/file_link_sync_service_spec.rb index 49c6cc8211f..b42b9653e53 100644 --- a/modules/storages/spec/services/storages/file_links/file_link_sync_service_spec.rb +++ b/modules/storages/spec/services/storages/file_links/file_link_sync_service_spec.rb @@ -29,311 +29,146 @@ require 'spec_helper' require 'webmock/rspec' -# The FileLinkSyncService takes an array of FileLink models and -# performs a REST call to a Nextcloud server to check which of the -# links are visible to the current user ("shared_with_me"). -# We want to test that permissions are processed correctoy and also -# test the reaction to various types of network issues. -# This spec bears some similarities to the connection_manager_spec.rb. RSpec.describe Storages::FileLinkSyncService, type: :model do let(:user) { create(:user) } let(:role) { create(:existing_role, permissions: [:manage_file_links]) } let(:project) { create(:project, members: { user => role }) } let(:work_package) { create(:work_package, project:) } - let(:filesinfo_path) { '/ocs/v1.php/apps/integration_openproject/filesinfo' } + let(:storage_one) { create(:storage, host: "https://host-1.example.org") } + let(:storage_two) { create(:storage, host: "https://host-2.example.org") } - # We want to check the case of file_links from multiple storages - let(:host1) { "http://host-1.example.org" } - let(:host2) { "http://host-2.example.org" } - let(:storage1) { create(:storage, host: host1) } - let(:storage2) { create(:storage, host: host2) } - let(:oauth_client1) { create(:oauth_client, integration: storage1) } - let(:oauth_client2) { create(:oauth_client, integration: storage2) } - let(:oauth_client_token1) { create(:oauth_client_token, oauth_client: oauth_client1, user:) } - let(:oauth_client_token2) { create(:oauth_client_token, oauth_client: oauth_client2, user:) } - let(:file_link1) do - create(:file_link, - origin_id: "24", - origin_updated_at: Time.zone.at(1655301000), - storage: storage1, - container: work_package) - end - let(:file_link2) do - create(:file_link, - origin_id: '25', - origin_updated_at: Time.zone.at(1655301000), - storage: storage2, - container: work_package) - end + let(:file_link_one) { create(:file_link, storage: storage_one, container: work_package) } + let(:file_link_two) { create(:file_link, storage: storage_two, container: work_package) } - # We're going to mock OAuth2 authentication failures below - let(:connection_manager) { OAuthClients::ConnectionManager.new(user:, oauth_client: oauth_client1) } - let(:authorize_url) { 'https://example.com/authorize' } - let(:instance) { described_class.new(user:) } + let(:file_links) { [file_link_one] } - # Indication from Nextcloud, true means that file has been deleted and should not be shown - let(:trashed) { false } - let(:file_links) { [file_link1] } + let(:files_info_query) { instance_double(Storages::Peripherals::StorageInteraction::Nextcloud::FilesInfoQuery) } - # Nextcloud response for valid file, changing all origin_* attributes - let(:file_info1_s200) do - { - id: 24, - status: "OK", - statuscode: 200, - ctime: 1755334567, # -> origin_created_at - mtime: 1755301234, # -> origin_updated_at - mimetype: "application/text", # -> origin_mime_type - name: "Readme.txt", # -> origin_name - owner_id: "fraber_id", - owner_name: "fraber", # -> origin_created_by_name - size: 1270, - trashed: - } - end - - # Meta information from Nextcloud as part of replies - per HTTP status code - let(:ocs_meta_s200) { { status: "ok", statuscode: 100, message: "OK", totalitems: "", itemsperpage: "" } } - let(:ocs_meta_s401) { { status: "failure", statuscode: 997, message: "No login", totalitems: "", itemsperpage: "" } } - - # Reply from Nextcloud if not allowed to access file: - # OpenProject should still show the file. - # This reply appears for example when some other user shares a file. - let(:file_info_s403) { { status: "Forbidden", statuscode: 403 } } - - # Reply from Nextcloud if Nextcloud internally can't find the file: - # OP should not show the file - # This reply appears if we send an invalid origin_id to Nextcloud files_info endpoint. - let(:file_info_s404) { { status: "Not Found", statuscode: 404 } } + subject { described_class.new(user:).call(file_links) } before do - oauth_client_token1 - oauth_client_token2 + storage_requests = instance_double(Storages::Peripherals::StorageRequests) + allow(storage_requests).to receive(:files_info_query).and_return(files_info_query) + allow(Storages::Peripherals::StorageRequests).to receive(:new).and_return(storage_requests) end - # Test the main function of the service, which is to - # the OAuth2 provider URL (Nextcloud) according to RFC specs. - describe '#call', webmock: true do - subject { instance.call(file_links) } + describe '#call' do + context 'with one file link' do + let(:file_info) { build(:storage_file_info) } + let(:file_link_one) { create(:file_link, origin_id: file_info.id, storage: storage_one, container: work_package) } - context 'with access tokens' do - context 'with one FileLink and one storage' do - let(:response) { { ocs: { meta: ocs_meta_s401, data: { '24': file_info1_s200 } } }.to_json } - - before do - # Simulate a successfully authorized reply with updates from Nextcloud - stub_request(:post, File.join(host1, filesinfo_path)) - .to_return(status: 200, headers: { 'Content-Type': 'application/json' }, body: response) - end - - it 'updates all origin_* fields' do - expect(subject.success).to be_truthy - expect(subject.result.count).to be 1 - expect(subject.result[0]).to be_a Storages::FileLink - - # Check the detailed update result - expect(subject.result[0].origin_id).to eql '24' - expect(subject.result[0].origin_created_at).to eql Time.zone.at(1755334567) - expect(subject.result[0].origin_updated_at).to eql Time.zone.at(1755301234) - expect(subject.result[0].origin_mime_type).to eql "application/text" - expect(subject.result[0].origin_name).to eql "Readme.txt" - expect(subject.result[0].origin_created_by_name).to eql "fraber" - end + before do + allow(files_info_query).to receive(:call).and_return(ServiceResult.success(result: [file_info])) end - context 'without permission to read file (403)' do - let(:response) { { ocs: { meta: ocs_meta_s200, data: { '24': file_info_s403 } } }.to_json } + it 'updates all origin_* fields' do + expect(subject.success).to be_truthy + expect(subject.result.count).to be 1 + expect(subject.result.first).to be_a Storages::FileLink - before do - stub_request(:post, File.join(host1, filesinfo_path)) - .to_return(status: 200, headers: { 'Content-Type': 'application/json' }, body: response) - end - - it 'returns a FileLink with #origin_permission :not_allowed' do - expect(subject.success).to be_truthy - expect(subject.result[0].origin_permission).to be :not_allowed - end - end - - context 'with 2 storages, each with one FileLink, one updated and other not allowed' do - let(:file_links) { [file_link1, file_link2] } - let(:response1) { { ocs: { meta: ocs_meta_s200, data: { '24': file_info1_s200 } } }.to_json } - let(:response2) { { ocs: { meta: ocs_meta_s200, data: { '25': file_info_s403 } } }.to_json } - - before do - # Simulate a successfully authorized reply with updates from Nextcloud - stub_request(:post, File.join(host1, filesinfo_path)) - .to_return(status: 200, headers: { 'Content-Type': 'application/json' }, body: response1) - stub_request(:post, File.join(host2, filesinfo_path)) - .to_return(status: 200, headers: { 'Content-Type': 'application/json' }, body: response2) - end - - it 'returns a successful ServiceResult with two FileLinks with different permissions' do - expect(subject.success).to be_truthy - expect(subject.result.count).to be 2 - expect(subject.result[0].origin_id).to eql '24' - expect(subject.result[1].origin_id).to eql '25' - expect(subject.result[0].origin_permission).to be :view - expect(subject.result[1].origin_permission).to be :not_allowed - end - end - - context 'when file was not found (404)' do - # Nextcloud returns 404 if it internally can't find the origin_id of the file. - # I'm not sure for reasons (internal error, ...), but the file shouldn't be shown then. - let(:file_links) { [file_link1] } - let(:response) { { ocs: { meta: ocs_meta_s200, data: { '24': file_info_s404 } } }.to_json } - - before do - # Simulate a successfully authorized reply with updates from Nextcloud - stub_request(:post, File.join(host1, filesinfo_path)) - .to_return(status: 200, headers: { 'Content-Type': 'application/json' }, body: response) - end - - it 'deletes the FileLink' do - expect(subject.success).to be_truthy - expect(subject.result.count).to be 0 - expect(Storages::FileLink.all.count).to be 0 - end - end - - context 'with connection timeout Nextcloud' do - before do - # Simulate a complete disconnection - stub_request(:any, File.join(host1, filesinfo_path)) - .to_timeout - end - - it 'leaves the list of file_links unchanged with permissions = :error' do - expect(subject.success).to be_falsey - expect(subject.result[0].origin_permission).to be :error - end - end - - context 'with connection to Nextcloud1, permission from Nextcloud1 and some updates' do - let(:response) { { ocs: { meta: ocs_meta_s200, data: { '24': file_info1_s200 } } }.to_json } - - before do - stub_request(:post, File.join(host1, filesinfo_path)) - .to_return(status: 200, headers: { 'Content-Type': 'application/json' }, body: response) - end - - # Just test a single update (mtime). Detailed update testing is further below. - it 'updates the file_link information' do - expect(subject).to be_a ServiceResult - expect(subject.success).to be_truthy - expect(subject.result[0].origin_updated_at).to eql Time.zone.at(1755301234) - end - - it 'updates the origin_permission' do - expect(subject.success).to be_truthy - expect(subject.result[0].origin_permission).to be :view # updated - end - end - - context 'with expired OAuth2 token, successful refresh and updated information from Nextcloud' do - before do - # Mock the OAuth2 connection manager to return a valid token - allow(OAuthClients::ConnectionManager) - .to receive(:new) - .and_return(connection_manager) - allow(connection_manager) - .to receive(:refresh_token) - .and_return(ServiceResult.success(result: oauth_client_token1)) - allow(connection_manager) - .to receive(:get_access_token) - .and_return(ServiceResult.success(result: oauth_client_token1)) - # We can't mock :request_with_token_refresh easily, as it takes a block - # of the instance to be tested. So we use a "real" ConnectionManager instance instead. - - # Mock Nextcloud to return: - # first: a 401 indicating an outdated OAuth2 Bearer token and - # then: a 200 with a reasonable result - stub_request(:post, File.join(host1, filesinfo_path)) - .to_return(status: 401, - headers: { 'Content-Type': 'application/json; charset=utf-8' }, - body: { ocs: { meta: ocs_meta_s401, data: [] } }.to_json) - .times(1) - .then - .to_return(status: 200, - headers: { 'Content-Type': 'application/json; charset=utf-8' }, - body: { ocs: { meta: ocs_meta_s200, data: { '24': file_info1_s200 } } }.to_json) - end - - # Just check that some update has happened. - it 'updates the file_link information' do - expect(subject).to be_a ServiceResult - expect(subject.success).to be_truthy - expect(subject.result[0].origin_updated_at).to eql Time.zone.at(1755301234) - expect(connection_manager).to have_received(:refresh_token).once - end - end - - context 'with expired OAuth2 token and refresh failed' do - before do - # Mock Nextcloud to return 401 (not authorized) indicating an expired OAuth2 Bearer token - stub_request(:post, File.join(host1, filesinfo_path)) - .to_return(status: 401, - headers: { 'Content-Type': 'application/json; charset=utf-8' }, - body: { ocs: { meta: ocs_meta_s401, data: [] } }.to_json) - - # Mock the OAuth2 connection manager to return some token on :get_access_token - # (that is considered 401-not authorized in the WebMock above) and then - # to return a failed ServiceResult on :refresh_token. - allow(OAuthClients::ConnectionManager) - .to receive(:new).and_return(connection_manager) - allow(connection_manager) - .to receive(:refresh_token) - .and_return( - ServiceResult.failure.tap do |result| - result.errors.add(:base, "Error from Nextcloud") - end - ) - allow(connection_manager) - .to receive(:get_access_token).and_return(ServiceResult.success(result: oauth_client_token1)) - end - - it 'returns a failed ServiceResult with an error' do - expect(subject).to be_a ServiceResult - expect(subject.success).to be_falsey - expect(subject.message).to include "Error from Nextcloud" - end - end - - context 'with FileLink trashed in nextcloud' do - let(:trashed) { true } # trashed is included in body: response below - let(:response) { { ocs: { meta: ocs_meta_s200, data: { '24': file_info1_s200 } } }.to_json } - - before do - stub_request(:post, File.join(host1, filesinfo_path)) - .to_return(status: 200, headers: { 'Content-Type': 'application/json' }, body: response) - end - - it 'returns an empty list of FileLinks' do - expect(subject).to be_a ServiceResult - expect(subject.success).to be_truthy - expect(subject.result.length).to be 0 - end + expect(subject.result.first.origin_id).to eql file_info.id + expect(subject.result.first.origin_created_at).to eql file_info.created_at + expect(subject.result.first.origin_updated_at).to eql file_info.last_modified_at + expect(subject.result.first.origin_mime_type).to eql file_info.mime_type + expect(subject.result.first.origin_name).to eql file_info.name + expect(subject.result.first.origin_created_by_name).to eql file_info.owner_name end end - context 'without access token' do - # Simulate a Storage for which we don't have an access token at all - # We don't need to Web-mock anything, we just don't setup an oauth_client_token below. - let(:host3) { "http://host-3.example.org" } - let(:storage3) { create(:storage, host: host3) } - let(:oauth_client3) { create(:oauth_client, integration: storage3) } - # Please notice the missing let(:oauth_client_token3) here - let(:file_link3) { create(:file_link, storage: storage3, container: work_package) } - let(:file_links) { [file_link3] } + context 'without permission to read file (403)' do + let(:file_info) { build(:storage_file_info, status_code: 403) } + let(:file_link_one) { create(:file_link, origin_id: file_info.id, storage: storage_one, container: work_package) } before do - oauth_client3 + allow(files_info_query).to receive(:call).and_return(ServiceResult.success(result: [file_info])) end - it 'returns a falsey ServiceResults coming from ConnectionManager' do - expect(subject.success).to be_falsey + it 'returns a FileLink with #origin_permission :not_allowed' do + expect(subject.success).to be_truthy + expect(subject.result.first.origin_permission).to be :not_allowed + end + end + + context 'with two file links, one updated and other not allowed' do + let(:file_info_one) { build(:storage_file_info) } + let(:file_info_two) { build(:storage_file_info, status_code: 403) } + + let(:file_link_one) { create(:file_link, origin_id: file_info_one.id, storage: storage_one, container: work_package) } + let(:file_link_two) { create(:file_link, origin_id: file_info_two.id, storage: storage_two, container: work_package) } + + let(:file_links) { [file_link_one, file_link_two] } + + before do + allow(files_info_query).to receive(:call).and_return(ServiceResult.success(result: [file_info_one, file_info_two])) + end + + it 'returns a successful result with two file links with different permissions' do + expect(subject.success).to be_truthy + expect(subject.result.count).to be 2 + expect(subject.result[0].origin_id).to eql file_info_one.id + expect(subject.result[1].origin_id).to eql file_info_two.id + expect(subject.result[0].origin_permission).to be :view + expect(subject.result[1].origin_permission).to be :not_allowed + end + end + + context 'when file was not found (404)' do + let(:file_info) { build(:storage_file_info, status_code: 404) } + let(:file_link_one) { create(:file_link, origin_id: file_info.id, storage: storage_one, container: work_package) } + + before do + allow(files_info_query).to receive(:call).and_return(ServiceResult.success(result: [file_info])) + end + + it 'deletes the file link' do + expect(subject.success).to be_truthy + expect(subject.result.count).to be 0 + expect(Storages::FileLink.all.count).to be 0 + end + end + + context 'when file has a different error (555)' do + let(:file_info) { build(:storage_file_info, status_code: 555) } + let(:file_link_one) { create(:file_link, origin_id: file_info.id, storage: storage_one, container: work_package) } + + before do + allow(files_info_query).to receive(:call).and_return(ServiceResult.success(result: [file_info])) + end + + it 'returns the file link with a permission set to :error' do + expect(subject.success).to be_truthy + expect(subject.result.count).to be 1 + expect(Storages::FileLink.all.count).to be 1 + expect(subject.result.first.origin_permission).to be :error + end + end + + context 'with files_info_query failing' do + before do + allow(files_info_query).to receive(:call).and_return( + ServiceResult.failure(result: :error, errors: Storages::StorageError.new(code: :error)) + ) + end + + it 'leaves the list of file_links unchanged with permissions = :error' do + expect(subject.success).to be_truthy + expect(subject.result.first.origin_permission).to be :error + end + end + + context 'with file trashed in storage' do + let(:file_info) { build(:storage_file_info, trashed: true) } + let(:file_link_one) { create(:file_link, origin_id: file_info.id, storage: storage_one, container: work_package) } + + before do + allow(files_info_query).to receive(:call).and_return(ServiceResult.success(result: [file_info])) + end + + it 'returns an empty list of FileLinks' do + expect(subject).to be_a ServiceResult + expect(subject.success).to be_truthy + expect(subject.result.length).to be 0 end end end diff --git a/modules/storages/spec/services/storages/group_folder_properties_sync_service_spec.rb b/modules/storages/spec/services/storages/group_folder_properties_sync_service_spec.rb index fa16274f242..4682d5309df 100644 --- a/modules/storages/spec/services/storages/group_folder_properties_sync_service_spec.rb +++ b/modules/storages/spec/services/storages/group_folder_properties_sync_service_spec.rb @@ -322,14 +322,14 @@ RSpec.describe Storages::GroupFolderPropertiesSyncService, webmock: true do host: 'https://example.com', password: '12345678') end - let(:projects_storage1) do + let(:project_storage1) do create(:project_storage, project_folder_mode: 'automatic', project: project1, storage:) end - let(:projects_storage2) do + let(:project_storage2) do create(:project_storage, project_folder_mode: 'automatic', project: project2, @@ -383,7 +383,7 @@ RSpec.describe Storages::GroupFolderPropertiesSyncService, webmock: true do body: propfind_request_body, headers: { 'Authorization' => 'Basic T3BlblByb2plY3Q6MTIzNDU2Nzg=', - 'Depth' => '0' + 'Depth' => '1' } ).to_return(status: 207, body: propfind_response_body2, headers: {}) request_stubs << stub_request(:post, "https://example.com/ocs/v1.php/cloud/users/Obi-Wan/groups") @@ -447,16 +447,16 @@ RSpec.describe Storages::GroupFolderPropertiesSyncService, webmock: true do end it 'sets project folders properties' do - expect(projects_storage1.project_folder_id).to be_nil - expect(projects_storage2.project_folder_id).to eq('123') + expect(project_storage1.project_folder_id).to be_nil + expect(project_storage2.project_folder_id).to eq('123') described_class.new(storage).call expect(request_stubs).to all have_been_requested - projects_storage1.reload - projects_storage2.reload - expect(projects_storage1.project_folder_id).to eq('819') - expect(projects_storage2.project_folder_id).to eq('123') + project_storage1.reload + project_storage2.reload + expect(project_storage1.project_folder_id).to eq('819') + expect(project_storage2.project_folder_id).to eq('123') end context 'when remove_user_from_group_command fails unexpectedly' do @@ -475,18 +475,18 @@ RSpec.describe Storages::GroupFolderPropertiesSyncService, webmock: true do end it 'sets project folders properties, but does not remove inactive user from group' do - expect(projects_storage1.project_folder_id).to be_nil - expect(projects_storage2.project_folder_id).to eq('123') + expect(project_storage1.project_folder_id).to be_nil + expect(project_storage2.project_folder_id).to eq('123') expect do described_class.new(storage).call end.to raise_error(RuntimeError, /remove_user_from_group_command was called with/) expect(request_stubs).to all have_been_requested - projects_storage1.reload - projects_storage2.reload - expect(projects_storage1.project_folder_id).to eq('819') - expect(projects_storage2.project_folder_id).to eq('123') + project_storage1.reload + project_storage2.reload + expect(project_storage1.project_folder_id).to eq('819') + expect(project_storage2.project_folder_id).to eq('123') end end end diff --git a/modules/storages/spec/services/storages/project_storages/create_service_spec.rb b/modules/storages/spec/services/storages/project_storages/create_service_spec.rb index b619b4e3969..3d2855ab050 100644 --- a/modules/storages/spec/services/storages/project_storages/create_service_spec.rb +++ b/modules/storages/spec/services/storages/project_storages/create_service_spec.rb @@ -28,12 +28,12 @@ require 'spec_helper' require 'services/base_services/behaves_like_create_service' -require_relative 'shared_synchronization_trigger_examples' +require_relative 'shared_event_gun_examples' RSpec.describe Storages::ProjectStorages::CreateService, type: :model do it_behaves_like 'BaseServices create service' do let(:factory) { :project_storage } - it_behaves_like 'a nextcloud synchronization trigger' + it_behaves_like('an event gun', OpenProject::Events::PROJECT_STORAGE_CREATED) end end diff --git a/modules/storages/spec/services/storages/project_storages/delete_service_spec.rb b/modules/storages/spec/services/storages/project_storages/delete_service_spec.rb index a530dfb1482..e4277f873ec 100644 --- a/modules/storages/spec/services/storages/project_storages/delete_service_spec.rb +++ b/modules/storages/spec/services/storages/project_storages/delete_service_spec.rb @@ -29,7 +29,7 @@ require 'spec_helper' require_module_spec_helper require 'services/base_services/behaves_like_delete_service' -require_relative 'shared_synchronization_trigger_examples' +require_relative 'shared_event_gun_examples' RSpec.describe Storages::ProjectStorages::DeleteService, type: :model, webmock: true do context 'with records written to DB' do @@ -51,8 +51,7 @@ RSpec.describe Storages::ProjectStorages::DeleteService, type: :model, webmock: project_storage described_class.new(model: project_storage, user:).call - expect(Storages::ProjectStorage.where(id: project_storage.id)) - .not_to exist + expect(Storages::ProjectStorage.where(id: project_storage.id)).not_to exist end it 'deletes all FileLinks that belong to containers of the related project' do @@ -61,51 +60,42 @@ RSpec.describe Storages::ProjectStorages::DeleteService, type: :model, webmock: described_class.new(model: project_storage, user:).call - expect(Storages::FileLink.where(id: file_link.id)) - .not_to exist - expect(Storages::FileLink.where(id: other_file_link.id)) - .to exist + expect(Storages::FileLink.where(id: file_link.id)).not_to exist + expect(Storages::FileLink.where(id: other_file_link.id)).to exist end context 'with Nextcloud storage' do let(:storage) { create(:nextcloud_storage) } - - before do + let(:delete_folder_url) do + "#{storage.host}/remote.php/dav/files/#{storage.username}/#{project_storage.project_folder_path.chop}" + end + let(:delete_folder_stub) do stub_request(:delete, delete_folder_url).to_return(status: 204, body: nil, headers: {}) end + before { delete_folder_stub } + it 'tries to remove the project folder at the external nextcloud storage' do expect(described_class.new(model: project_storage, user:).call).to be_success + expect(delete_folder_stub).to have_been_requested end - context 'if project folder is not present' do - before do + context 'if project folder deletion request fails' do + let(:delete_folder_stub) do stub_request(:delete, delete_folder_url).to_return(status: 404, body: nil, headers: {}) end it 'tries to remove the project folder at the external nextcloud storage and still succeed with deletion' do expect(described_class.new(model: project_storage, user:).call).to be_success - end - end - - context 'if access is not authorized' do - before do - stub_request(:delete, delete_folder_url).to_return(status: 401, body: nil, headers: {}) - end - - it 'tries to remove the project folder at the external nextcloud storage, fails and does not delete project storage' do - expect(described_class.new(model: project_storage, user:).call).not_to be_success - expect(Storages::ProjectStorage.where(id: project_storage.id)).to exist + expect(delete_folder_stub).to have_been_requested end end end end - # Includes many specs that are common for every DeleteService that inherits from ::BaseServices::Delete. - # Collected tests on DeleteContracts from last 15 years. it_behaves_like 'BaseServices delete service' do let(:factory) { :project_storage } - it_behaves_like 'a nextcloud synchronization trigger' + it_behaves_like('an event gun', OpenProject::Events::PROJECT_STORAGE_DESTROYED) end end diff --git a/modules/storages/spec/services/storages/project_storages/shared_event_gun_examples.rb b/modules/storages/spec/services/storages/project_storages/shared_event_gun_examples.rb new file mode 100644 index 00000000000..1bd47653020 --- /dev/null +++ b/modules/storages/spec/services/storages/project_storages/shared_event_gun_examples.rb @@ -0,0 +1,44 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +RSpec.shared_examples 'an event gun' do |event| + %i[automatic manual inactive].each do |mode| + context "when project_folder mode is #{mode}" do + it 'fires an appropriate event' do + allow(OpenProject::Notifications).to(receive(:send)) + model_instance.project_folder_mode = mode + + subject + + expect(OpenProject::Notifications).to( + have_received(:send).with(event, project_folder_mode: mode) + ) + end + end + end +end diff --git a/modules/storages/spec/services/storages/project_storages/update_service_spec.rb b/modules/storages/spec/services/storages/project_storages/update_service_spec.rb index 1e8493ce1ba..48efb889b54 100644 --- a/modules/storages/spec/services/storages/project_storages/update_service_spec.rb +++ b/modules/storages/spec/services/storages/project_storages/update_service_spec.rb @@ -28,12 +28,12 @@ require 'spec_helper' require 'services/base_services/behaves_like_update_service' -require_relative 'shared_synchronization_trigger_examples' +require_relative 'shared_event_gun_examples' RSpec.describe Storages::ProjectStorages::UpdateService, type: :model do it_behaves_like 'BaseServices update service' do let(:factory) { :project_storage } - it_behaves_like 'a nextcloud synchronization trigger' + it_behaves_like('an event gun', OpenProject::Events::PROJECT_STORAGE_UPDATED) end end diff --git a/modules/storages/spec/workers/storages/manage_nextcloud_integration_events_job_spec.rb b/modules/storages/spec/workers/storages/manage_nextcloud_integration_events_job_spec.rb index f34fac7f63a..1b168095dd4 100644 --- a/modules/storages/spec/workers/storages/manage_nextcloud_integration_events_job_spec.rb +++ b/modules/storages/spec/workers/storages/manage_nextcloud_integration_events_job_spec.rb @@ -34,7 +34,7 @@ RSpec.describe Storages::ManageNextcloudIntegrationEventsJob, type: :job do ActiveJob::Base.disable_test_adapter other_handler = Storages::ManageNextcloudIntegrationCronJob.perform_later.provider_job_id - same_handler_within_timeframe1 = described_class.set(wait: 1.seconds).perform_later.provider_job_id + same_handler_within_timeframe1 = described_class.set(wait: 1.second).perform_later.provider_job_id same_handler_within_timeframe2 = described_class.set(wait: 2.seconds).perform_later.provider_job_id same_handler_within_timeframe3 = described_class.set(wait: 3.seconds).perform_later.provider_job_id same_handler_out_of_timeframe = described_class.set(wait: 1.minute).perform_later.provider_job_id diff --git a/modules/team_planner/config/locales/crowdin/nl.yml b/modules/team_planner/config/locales/crowdin/nl.yml index cb65b5a5b86..fd792d27762 100644 --- a/modules/team_planner/config/locales/crowdin/nl.yml +++ b/modules/team_planner/config/locales/crowdin/nl.yml @@ -5,7 +5,7 @@ nl: project_module_team_planner_view: "Team planners" team_planner: label_team_planner: "Team planner" - label_new_team_planner: "New team planner" + label_new_team_planner: "Nieuwe teamplanner" label_create_new_team_planner: "Maak een nieuwe teamplanner" label_team_planner_plural: "Team planners" label_assignees: "Toegewezen personen" diff --git a/script/ci/setup.sh b/script/ci/setup.sh index fab0418f768..e3c299b222a 100644 --- a/script/ci/setup.sh +++ b/script/ci/setup.sh @@ -56,12 +56,6 @@ if [ $1 = 'npm' ]; then echo "No asset compilation required" fi -if [ $1 = 'units' ]; then - # Install pandoc for testing textile migration - run "sudo apt-get update -qq" - run "sudo apt-get install -qq pandoc" -fi - if [ ! -f "public/assets/frontend_assets.manifest.json" ]; then if [ -z "${RECOMPILE_ON_TRAVIS_CACHE_ERROR}" ]; then echo "ERROR: asset manifest was not properly cached. exiting" diff --git a/spec/components/docs/01-how-to-use.md.erb b/spec/components/docs/01-how-to-use.md.erb new file mode 100644 index 00000000000..63acef64e9a --- /dev/null +++ b/spec/components/docs/01-how-to-use.md.erb @@ -0,0 +1,24 @@ +--- +title: Using this resource +--- + +We use this lookbook to document the use and adaptation of the [Primer Design System](https://primer.style/) at [OpenProject](https://www.openproject.org). +You will find defined styles, components and patterns in the relevant sections. + +## Approach + +OpenProject is a complex, powerful tool. One of its key strengths is its customisability and its ability to adapt to a range of different needs. This includes complex filtering options, custom types and statuses, custom fields and a wide range of options to configure views and work package forms. + +Nevertheless, it is very important that OpenProject be intuitive for new users who might not necessarily need that complexity, or indeed be overwhelmed by it. + +Our design approach aims to strike the right balance between powerful and accessible with a two-tiered approach: apply sane defaults and present the most common options, and allow advanced users the option (via an additional click) to customise and fine-tune. + +## The UX of Open Source + +As an open source project with a considerably long history and a large number of contributors, different parts of OpenProject have evolved at different paces, sometimes with completely different technology. Similar components are sometimes implemented somewhat differently in different parts of the software, and there are even multiple implementations of the same basic design. + +This is quite normal for a large open-source project that has not had a dedicated design team for most of its conception. + +One of the goals of the design system is to introduce more coherence and introduce a more modern design language. Whilst we would naturally prefer to be able to update everything at the same time and push the new design system to the entire software, we recognise the need for a more pragmatic approach. The design system will be rolled out in phases, with a careful study of the consequences of updating each component or pattern, and the potential dependencies that will be affected. + +_We recognise that UI/UX has not always been the highest priority for open-source projects. This is somewhat understandable given how open source projects have relatively fewer design resources dedicated to it than commercial products. Our goal is to do our part to improve that situation as much as we can and document our process._ diff --git a/spec/components/docs/02-semantics.erb b/spec/components/docs/02-semantics.erb new file mode 100644 index 00000000000..fd970a4f7d1 --- /dev/null +++ b/spec/components/docs/02-semantics.erb @@ -0,0 +1,29 @@ +--- +title: Semantics +--- + +When describing components, we use certain words in very particular ways: + +### Disabled + +_Disabled_ is when no interaction is possible. A disabled element can itself have multiple states: a checkbox can be checked and disabled, a switch can be ON and disabled, a dropdown might have a value but but disabled. + +The opposite of disabled is _enabled_. All elements are described in their enabled state unless explicitly mentioned. + +### Focused and active + +Focus simply refers to when an element has the current object in DOM that has the input focus. In OpenProject, elements that are focused generally have a blue outline. + +The opposite of _in focus_ (or focused) is _not in focus_ (or focused). + +_Active_ is a similar state as focused, but not entirely the same. For example, in the date picker, the start date can be "active" (meaning that clicking on a date in the mini-calendar will change the start date) but not in focus (because the mouse has clicked outside of the field). + +The opposite of active is _inactive_. + +### Checked and on + +Checkboxes can be _checked_ (true) or _unchecked_ (false) + +Radio options can also be "checked" (as in, in a set of three options, only one can be checked) despite this not being the technically correct word. This isi to to distinguish it from "selected" (which refers to selecting an element or an area with the cursor). + +Switches can be _ON_ (true) or _OFF_ (false). diff --git a/frontend/src/stories/ModalDialogue.mdx b/spec/components/docs/patterns/modals.md.erb similarity index 94% rename from frontend/src/stories/ModalDialogue.mdx rename to spec/components/docs/patterns/modals.md.erb index 01e2c2cdadb..e2e89ab9ab2 100644 --- a/frontend/src/stories/ModalDialogue.mdx +++ b/spec/components/docs/patterns/modals.md.erb @@ -1,10 +1,7 @@ -import { Meta } from '@storybook/blocks'; - +<%= embed ModalPreview, :danger_zone %> -# Modal dialogue - -> Example of modal with two buttons: Cancel and Delete (danger) +> Example of modal with two buttons: Cancel and Continue (danger) The modal dialogue is used to to provide actions that require the user’s attention. They interrupt the user’s regular navigation in that they cover the screen and make interaction behind it not possible whilst it is displayed. @@ -35,7 +32,7 @@ This component does not by itself define the types of content it can contain. So ## Behaviour -The modal is launched by user action and displayed in a lightbox (a semi-transparent grey background). +The modal is launched by user action and displayed in a lightbox (a semi-transparent grey background). The action bar always has only two actions, one of which is always “Cancel” (secondary) and the other one usually an confirmational action like “Apply” or “Save”. As with any action bar, a third action (usually a checkbox) can optionally be displayed on the left corner. @@ -59,7 +56,7 @@ The container has 4px rounded corners. The modal (in desktop form) has two acceptable widths: 40 REM and 60 REM. -The header has 16px padding (left and right) and a 16px top margin. +The header has 16px padding (left and right) and a 16px top margin. Note that the optional divider in the header a 16px top margin but no additional margin/padding, and must take 100% of the width of the modal. diff --git a/frontend/src/stories/Typography.mdx b/spec/components/docs/styles/01-typography.md.erb similarity index 59% rename from frontend/src/stories/Typography.mdx rename to spec/components/docs/styles/01-typography.md.erb index 47306307a3a..f6fade140af 100644 --- a/frontend/src/stories/Typography.mdx +++ b/spec/components/docs/styles/01-typography.md.erb @@ -1,8 +1,8 @@ -import { Meta } from '@storybook/blocks'; +--- +title: Typography +--- - - -# Typography +<%= include_spot_assets %> OpenProject uses the "[Lato Sans](https://github.com/latofonts/lato-source)" typeface created by Adam Twardoch, Botio Nikoltchev, and Łukasz Dziedzic (released with the [SIL Open Font license](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL)). @@ -28,13 +28,37 @@ This is a larger header than the default, used in rare occasions where a header So far, zero recorded use. Might get removed if this style is not used in until mid-2022. -> Code example here +
      +

      + Aa +
      + ABCDEFGHIJKLMNOPQRSTUVWXYZ +
      + abcdefghijklmnopqrstuvwxyz +
      + 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

      +
      + + ## Header Small This is the default type used for page headers. "Bold" is default and is almost always preferred, unless there is a need to use regular to distinguish additional or supporting information. -> Code example here +
      +

      + Aa +
      + ABCDEFGHIJKLMNOPQRSTUVWXYZ +
      + abcdefghijklmnopqrstuvwxyz +
      + 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

      +
      + + ## Subheader Big @@ -42,23 +66,71 @@ Very rarely used. Used if we need a header small than the regular header but sti So far, zero recorded use. Might get removed if this style is not used in until mid-2022. -> Code example here +
      +

      + Aa +
      + ABCDEFGHIJKLMNOPQRSTUVWXYZ +
      + abcdefghijklmnopqrstuvwxyz +
      + 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

      +
      + + ## Subheader Small Very rarely used. Used if we need a header small than the regular header but still distinct from body text. +
      +

      + Aa +
      + ABCDEFGHIJKLMNOPQRSTUVWXYZ +
      + abcdefghijklmnopqrstuvwxyz +
      + 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

      +
      + + + ## Subheader Extra Small This is really an alias of _Body Small/Bold_ when this style is used as a header (in some modals) instead of body text. -> Code example here +
      +

      + Aa +
      + ABCDEFGHIJKLMNOPQRSTUVWXYZ +
      + abcdefghijklmnopqrstuvwxyz +
      + 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

      +
      + + ## Body Big Used occasionally (where?) -> Code example here +
      +

      + Aa +
      + ABCDEFGHIJKLMNOPQRSTUVWXYZ +
      + abcdefghijklmnopqrstuvwxyz +
      + 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

      +
      ## Body Small @@ -78,7 +150,19 @@ The bold version is used in: - Calendar header - Sidebar header -> Code example here +**Example** + +
      +

      + Aa +
      + ABCDEFGHIJKLMNOPQRSTUVWXYZ +
      + abcdefghijklmnopqrstuvwxyz +
      + 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

      +
      ## Caption @@ -95,5 +179,17 @@ The bold version is used in: - Sidebar tabs -> Code example here +**Example** + +
      +

      + Aa +
      + ABCDEFGHIJKLMNOPQRSTUVWXYZ +
      + abcdefghijklmnopqrstuvwxyz +
      + 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

      +
      diff --git a/spec/components/docs/styles/02-icons.md.erb b/spec/components/docs/styles/02-icons.md.erb new file mode 100644 index 00000000000..77c7a1f5d31 --- /dev/null +++ b/spec/components/docs/styles/02-icons.md.erb @@ -0,0 +1,61 @@ +--- +title: Icons +--- + +# Icons + +Icons are used as visual indicators or small illustrations. They provide additional context when necessary. + +<% %w[person plus pencil].each do |icon| %> +<%= render(Primer::Beta::Octicon.new(icon: icon)) %> +
      +<% end %> + +## Design notes + +Please see the primer documentation for [icons](https://primer.style/design/foundations/icons/) and their [guidelines](https://primer.style/design/foundations/icons/design-guidelines). + +## Available icons + +
      +<% Octicons::OCTICON_SYMBOLS.keys.each do |name| %> +
      +
      + <%= render(Primer::Beta::Octicon.new(icon: name)) %> +
      +
      <%= name %>
      +
      +<% end %> +
      + + diff --git a/frontend/src/stories/Colors.mdx b/spec/components/docs/styles/03-colors.md.erb similarity index 88% rename from frontend/src/stories/Colors.mdx rename to spec/components/docs/styles/03-colors.md.erb index f23eb24be48..547e5416614 100644 --- a/frontend/src/stories/Colors.mdx +++ b/spec/components/docs/styles/03-colors.md.erb @@ -1,27 +1,8 @@ -import { Meta, Story, Canvas } from '@storybook/addon-docs'; -import tokens from '../app/spot/styles/tokens/dist/tokens.json'; +--- +title: Colors +--- -export const ColorsPreview = ({ tokens }) =>
      - {Object.keys(tokens).filter(key => key.startsWith('spot-color-')).map(name => ( -
      -
      -
      ${name}
      -
      {tokens[name]}
      -
      - ))} -
      ; - - - -# Colors (WIP) - -**Note**: This section is a work-in-progress. If you are unsure about which colour to use, please get in touch with the UX team. +**Note**: This section concerns the OP colors in use, and is about to change/consolidated with Primer color modes Because OpenProject can be customised with custom colour schemes, our foundation library only describe the colour palette of the default OpenProject theme. @@ -202,11 +183,23 @@ This colour is used to indicate additional information that could be of interest This colour indicates that the requested user action was successful. It should be used sparingly and only when such a feedback is absolutely required. - +
      +<% +tokens = JSON.parse File.read(Rails.root.join('frontend/src/app/spot/styles/tokens/dist/tokens.json')) +tokens.select { |k, v| k.start_with?('spot-color-') }.each do |name, value| + %> + +
      +
      +
      <%= name %>
      +
      <%= value %>
      +
      +<% end %> + +
      diff --git a/spec/components/docs/styles/04-spacings.md.erb b/spec/components/docs/styles/04-spacings.md.erb new file mode 100644 index 00000000000..522aae160c2 --- /dev/null +++ b/spec/components/docs/styles/04-spacings.md.erb @@ -0,0 +1,58 @@ +--- +title: Spacings +--- + +**Note:** These are still SPOT-defined spacings and need to be adapted for primer. + +
      +<% +tokens = JSON.parse File.read(Rails.root.join('frontend/src/app/spot/styles/tokens/dist/tokens.json')) +tokens + .select { |k, v| k.start_with?('spot-spacing-') } + .sort_by { |k, | k.split('-')[2].gsub('_', '.').to_f } + .each do |name, value| + %> +
      +
      <%= name %>
      +
      <%= value %>
      +
      +
      +<% end %> +
      + + diff --git a/frontend/src/stories/Shadows.mdx b/spec/components/docs/styles/05-shadows.md.erb similarity index 58% rename from frontend/src/stories/Shadows.mdx rename to spec/components/docs/styles/05-shadows.md.erb index f97ee33874b..19831d8a435 100644 --- a/frontend/src/stories/Shadows.mdx +++ b/spec/components/docs/styles/05-shadows.md.erb @@ -1,37 +1,8 @@ -import { Meta } from '@storybook/blocks'; -import tokens from '../app/spot/styles/tokens/dist/tokens.json'; -import { rows, cols } from './shadows-data.jsx'; +--- +title: Shadows +--- -export const ShadowsTable = ({ tokens }) => ( - - - - {cols.map((col) => )} - - {rows - .map((row) => ( - - {cols - .map((col) => `spot-shadow-${row.toLowerCase()}-${col.toLowerCase()}`) - .map((name) => ()) - } - )) - } - -
      {col}
      {row} -
      -
      ${name}
      -
      {tokens[name]}
      -
      -
      ); - - - - -# Shadows +**Note:** These are still SPOT-defined shadows and need to be replaced by primer Shadows are important when certain components are displayed on top of other components. This is usually the case with contextual menus, drop-downs or dialogues that supplement or expand an existing view. @@ -41,10 +12,43 @@ We use different shadows to communicate depth and allow the user to intuitively Our shadows definitions divided between Light and Hard and three levels of elevation. The shadow is always based on a black #000000 transparency level, a X and Y px value and a spread px value. - +<% +cols = ['Low', 'Mid', 'High'] +rows = ['Light', 'Hard'] +tokens = JSON.parse File.read(Rails.root.join('frontend/src/app/spot/styles/tokens/dist/tokens.json')) +tokens = tokens + .select { |k, v| k.start_with?('spot-shadow-') } + .sort_by { |k, | k.split('-')[2].gsub('_', '.').to_f } + .to_h + %> + + + + + + <% cols.each do |col| %> + + <% end %> + + <% rows.each do |row| %> + + + <% cols.each do |col| %> + <% key = "spot-shadow-#{row.downcase}-#{col.downcase}" %> + + <% end %> + +<% end %> + +
      <%= col %>
      <%= row %> +
      +
      <%= key %>
      +
      <%= tokens[key] %>
      +
      +
      + diff --git a/frontend/src/stories/Focus.mdx b/spec/components/docs/styles/06-focus.md.erb similarity index 74% rename from frontend/src/stories/Focus.mdx rename to spec/components/docs/styles/06-focus.md.erb index 224d8213416..6d0139ca20a 100644 --- a/frontend/src/stories/Focus.mdx +++ b/spec/components/docs/styles/06-focus.md.erb @@ -1,13 +1,9 @@ -import { Meta, Story, Canvas } from '@storybook/addon-docs'; -import tokens from '../app/spot/styles/tokens/dist/tokens.json'; - - - -# Focus state +--- +title: Focus state +--- Focus state in OpenProject is represented by a 2-pixel outline with colour _Indication/Focus_ around the focused element. > Example of some elements with the focus state simulated - **Note**: _Active_ is a similar state as focused, but not entirely the same. For example, in the date picker, the start date can be "active" (meaning that clicking on a date in the mini-calendar will change the start date) but not in focus (because the mouse pointer has clicked outside of the field and the focus is technically outside the field). diff --git a/spec/components/previews/advanced_filters_preview.rb b/spec/components/previews/advanced_filters_preview.rb new file mode 100644 index 00000000000..ca95982c69e --- /dev/null +++ b/spec/components/previews/advanced_filters_preview.rb @@ -0,0 +1,4 @@ +# @logical_path OpenProject +class AdvancedFiltersPreview < Lookbook::Preview + def default; end +end diff --git a/frontend/src/global_styles/content/_advanced_filters.lsg b/spec/components/previews/advanced_filters_preview/default.html.erb similarity index 97% rename from frontend/src/global_styles/content/_advanced_filters.lsg rename to spec/components/previews/advanced_filters_preview/default.html.erb index d44da69818e..081891fa21e 100644 --- a/frontend/src/global_styles/content/_advanced_filters.lsg +++ b/spec/components/previews/advanced_filters_preview/default.html.erb @@ -1,8 +1,3 @@ -# Advanced filters - -``` -@full-width -
      Selected filters
        @@ -121,7 +116,7 @@
      - +
      @@ -150,7 +145,7 @@
      - +
      @@ -340,4 +335,3 @@
    -``` diff --git a/spec/components/previews/attribute_group_component_preview.rb b/spec/components/previews/attribute_group_component_preview.rb new file mode 100644 index 00000000000..807469e5e90 --- /dev/null +++ b/spec/components/previews/attribute_group_component_preview.rb @@ -0,0 +1,4 @@ +# @logical_path OpenProject +class AttributeGroupComponentPreview < Lookbook::Preview + def default; end +end diff --git a/spec/components/previews/attribute_group_component_preview/default.html.erb b/spec/components/previews/attribute_group_component_preview/default.html.erb new file mode 100644 index 00000000000..d4456278600 --- /dev/null +++ b/spec/components/previews/attribute_group_component_preview/default.html.erb @@ -0,0 +1,12 @@ +<%= render(AttributeGroups::AttributeGroupComponent.new) do |component| + component.with_header(title: "A Title") + + component.with_attribute(key: 'Some key', + value: 'A value') + + component.with_attribute(key: 'With icons', + value: op_icon('icon-checkmark')) + + component.with_attribute(key: 'With HTML', + value: link_to('OpenProject homepage', 'https://www.openproject.org')) +end %> diff --git a/spec/components/previews/badges_preview.rb b/spec/components/previews/badges_preview.rb new file mode 100644 index 00000000000..c6548ebdc1b --- /dev/null +++ b/spec/components/previews/badges_preview.rb @@ -0,0 +1,8 @@ +# @logical_path OpenProject +class BadgesPreview < Lookbook::Preview + def default; end + + def secondary; end + + def border_only; end +end diff --git a/spec/components/previews/badges_preview/border_only.html.erb b/spec/components/previews/badges_preview/border_only.html.erb new file mode 100644 index 00000000000..4d0a00416f7 --- /dev/null +++ b/spec/components/previews/badges_preview/border_only.html.erb @@ -0,0 +1,3 @@ +0 +1 +2 diff --git a/spec/components/previews/badges_preview/default.html.erb b/spec/components/previews/badges_preview/default.html.erb new file mode 100644 index 00000000000..4c9a12e70e4 --- /dev/null +++ b/spec/components/previews/badges_preview/default.html.erb @@ -0,0 +1,3 @@ +0 +1 +2 diff --git a/spec/components/previews/badges_preview/secondary.html.erb b/spec/components/previews/badges_preview/secondary.html.erb new file mode 100644 index 00000000000..f3ca39e552a --- /dev/null +++ b/spec/components/previews/badges_preview/secondary.html.erb @@ -0,0 +1,3 @@ +0 +1 +2 diff --git a/spec/components/previews/buttons_preview.rb b/spec/components/previews/buttons_preview.rb new file mode 100644 index 00000000000..e602f0298e3 --- /dev/null +++ b/spec/components/previews/buttons_preview.rb @@ -0,0 +1,18 @@ +# @logical_path OpenProject +class ButtonsPreview < Lookbook::Preview + def default; end + + def highlight; end + + def alternative_highlight; end + + def danger; end + + def active; end + + def transparent; end + + def sizes; end + + def link_like; end +end diff --git a/spec/components/previews/buttons_preview/active.html.erb b/spec/components/previews/buttons_preview/active.html.erb new file mode 100644 index 00000000000..f2700bc51ef --- /dev/null +++ b/spec/components/previews/buttons_preview/active.html.erb @@ -0,0 +1,13 @@ + + + + +
    + +Connect +Click to connect + + + diff --git a/spec/components/previews/buttons_preview/alternative_highlight.html.erb b/spec/components/previews/buttons_preview/alternative_highlight.html.erb new file mode 100644 index 00000000000..29259cc844c --- /dev/null +++ b/spec/components/previews/buttons_preview/alternative_highlight.html.erb @@ -0,0 +1,13 @@ + + + + +
    + +Create +Click to create + + + diff --git a/spec/components/previews/buttons_preview/danger.html.erb b/spec/components/previews/buttons_preview/danger.html.erb new file mode 100644 index 00000000000..54b12e973e1 --- /dev/null +++ b/spec/components/previews/buttons_preview/danger.html.erb @@ -0,0 +1 @@ + diff --git a/spec/components/previews/buttons_preview/default.html.erb b/spec/components/previews/buttons_preview/default.html.erb new file mode 100644 index 00000000000..2c4efdbfc4e --- /dev/null +++ b/spec/components/previews/buttons_preview/default.html.erb @@ -0,0 +1,13 @@ + + + + +
    + +Watch +Click to watch + + + diff --git a/spec/components/previews/buttons_preview/highlight.html.erb b/spec/components/previews/buttons_preview/highlight.html.erb new file mode 100644 index 00000000000..5e2df80c1b6 --- /dev/null +++ b/spec/components/previews/buttons_preview/highlight.html.erb @@ -0,0 +1,13 @@ + + + + +
    + +Apply +Click to apply + + + diff --git a/spec/components/previews/buttons_preview/link_like.html.erb b/spec/components/previews/buttons_preview/link_like.html.erb new file mode 100644 index 00000000000..4dfe3c3e0ae --- /dev/null +++ b/spec/components/previews/buttons_preview/link_like.html.erb @@ -0,0 +1,2 @@ + + diff --git a/spec/components/previews/buttons_preview/sizes.html.erb b/spec/components/previews/buttons_preview/sizes.html.erb new file mode 100644 index 00000000000..bb22f601bfd --- /dev/null +++ b/spec/components/previews/buttons_preview/sizes.html.erb @@ -0,0 +1,27 @@ +

    Tiny

    + + + + + + +

    Small

    + + + + + + +

    Default

    + + + + + + +

    Large

    + + + + + diff --git a/spec/components/previews/buttons_preview/transparent.html.erb b/spec/components/previews/buttons_preview/transparent.html.erb new file mode 100644 index 00000000000..8b9c9cdaa3c --- /dev/null +++ b/spec/components/previews/buttons_preview/transparent.html.erb @@ -0,0 +1,13 @@ + + + + +
    + +Comment +Click to comment + + + diff --git a/spec/components/previews/collapsible_section_preview.rb b/spec/components/previews/collapsible_section_preview.rb new file mode 100644 index 00000000000..0a4fb3b829b --- /dev/null +++ b/spec/components/previews/collapsible_section_preview.rb @@ -0,0 +1,4 @@ +# @logical_path OpenProject +class CollapsibleSectionPreview < Lookbook::Preview + def default; end +end diff --git a/frontend/src/global_styles/content/_collapsible_section.lsg b/spec/components/previews/collapsible_section_preview/default.html.erb similarity index 65% rename from frontend/src/global_styles/content/_collapsible_section.lsg rename to spec/components/previews/collapsible_section_preview/default.html.erb index fe63c3bf9d4..2bc49b6feee 100644 --- a/frontend/src/global_styles/content/_collapsible_section.lsg +++ b/spec/components/previews/collapsible_section_preview/default.html.erb @@ -1,21 +1,14 @@ -# Collapsible section - -``` + section-title="My section title"> -``` - -``` -``` diff --git a/spec/components/previews/context_menu_preview.rb b/spec/components/previews/context_menu_preview.rb new file mode 100644 index 00000000000..62b71440f35 --- /dev/null +++ b/spec/components/previews/context_menu_preview.rb @@ -0,0 +1,4 @@ +# @logical_path OpenProject +class ContextMenuPreview < Lookbook::Preview + def default; end +end diff --git a/spec/components/previews/context_menu_preview/default.html.erb b/spec/components/previews/context_menu_preview/default.html.erb new file mode 100644 index 00000000000..a770cf4b663 --- /dev/null +++ b/spec/components/previews/context_menu_preview/default.html.erb @@ -0,0 +1,55 @@ + diff --git a/spec/components/previews/contextual_info_text_preview.rb b/spec/components/previews/contextual_info_text_preview.rb new file mode 100644 index 00000000000..a34ec242306 --- /dev/null +++ b/spec/components/previews/contextual_info_text_preview.rb @@ -0,0 +1,4 @@ +# @logical_path OpenProject +class ContextualInfoTextPreview < Lookbook::Preview + def default; end +end diff --git a/spec/components/previews/contextual_info_text_preview/default.html.erb b/spec/components/previews/contextual_info_text_preview/default.html.erb new file mode 100644 index 00000000000..afbcd45291e --- /dev/null +++ b/spec/components/previews/contextual_info_text_preview/default.html.erb @@ -0,0 +1,4 @@ +

    + Some text here + (Some contextual information for the text) +

    diff --git a/spec/components/previews/danger_zone_preview.rb b/spec/components/previews/danger_zone_preview.rb new file mode 100644 index 00000000000..945cdb481e2 --- /dev/null +++ b/spec/components/previews/danger_zone_preview.rb @@ -0,0 +1,8 @@ +# @logical_path OpenProject +class FormsPreview < Lookbook::Preview + def default; end + + def bordered; end + + def compressed; end +end diff --git a/spec/components/previews/danger_zone_preview/default.html.erb b/spec/components/previews/danger_zone_preview/default.html.erb new file mode 100644 index 00000000000..9970042f875 --- /dev/null +++ b/spec/components/previews/danger_zone_preview/default.html.erb @@ -0,0 +1,100 @@ +
    + + +
    +

    +
    +

    Widget Box

    +
    +

    +

    This widget box can be used to display content belonging to one subject.

    + +
    + + +
    +

    +
    +

    Widget Box 2

    +
    +

    + +
    + + +
    +

    +
    +

    Widget Box 3

    +
    +

    +
    +

    + Lorem ipsum dolor sit amet, his ei propriae suscipit. + Sit in atqui accumsan ponderum, eum ut luptatum lobortis, has ei tota illud detraxit. +

    +
    + +
    + + +
    +

    +
    +

    Widget Box 4

    +
    +

    +
      +
    • Enum1
    • +
    • Enum2
    • +
    • Enum3
    • +
    + +
    + +
    diff --git a/spec/components/previews/datepicker_preview.rb b/spec/components/previews/datepicker_preview.rb new file mode 100644 index 00000000000..40a2c38fdae --- /dev/null +++ b/spec/components/previews/datepicker_preview.rb @@ -0,0 +1,52 @@ +# @logical_path OpenProject +class DatepickerPreview < Lookbook::Preview + ## + # **Single date picker** + # --------------------- + # The basic date picker is a key element in OpenProject and is displayed + # any time the user has to input a date. + # + # Basic date pickers are attached to existing date input fields + # and is displayed as a drop-down when that date input field is in focus. + # It consists of only the mini-calendar component. + # + # The basic date picker can also be placed inside modals + # like the work package date drop modal and the Baseline drop modal. + # All date picker are built on the [Flatpickr javascript library](https://flatpickr.js.org/). + # + # On mobile devices, the component will automatically degrade to the device's native date picker. + # + # For more complex implementations involving date pickers (the work package date field or the baseline modal), + # please refer to documentation concerning those specific features for notes on mobile-specific rendering. + # + # The library gives us certain functionality out of the box with a fairly high degree of customisation, + # but also introduces limits (which will be mentioned below when relevant). + # + # Please read the [Flatpickr documentation](https://flatpickr.js.org/instance-methods-properties-elements/) + # before using or contributing to date pickers. + # + # @param value date + def single(value: Time.zone.today.iso8601) + render_with_template(locals: { value: }) + end + + ## + # **Range date picker** + # --------------------- + # Range datepicker allow inputing a range (2023-02-09 - 2023-02-14), + # and shows the date picker with two months. + # + # All date picker are built on the [Flatpickr javascript library](https://flatpickr.js.org/). + # On mobile devices, the component will automatically degrade to two inputs with the device's native date picker. + # + # The library gives us certain functionality out of the box with a fairly high degree of customisation, + # but also introduces limits (which will be mentioned below when relevant). + # + # Please read the [Flatpickr documentation](https://flatpickr.js.org/instance-methods-properties-elements/) + # before using or contributing to date pickers. + # + # @param value text + def range(value: "#{Time.zone.today.iso8601} - #{Time.zone.today.iso8601}") + render_with_template(locals: { value: }) + end +end diff --git a/spec/components/previews/datepicker_preview/range.html.erb b/spec/components/previews/datepicker_preview/range.html.erb new file mode 100644 index 00000000000..559e35bba55 --- /dev/null +++ b/spec/components/previews/datepicker_preview/range.html.erb @@ -0,0 +1 @@ +<%= tag :'opce-range-date-picker', value: %> diff --git a/spec/components/previews/datepicker_preview/single.html.erb b/spec/components/previews/datepicker_preview/single.html.erb new file mode 100644 index 00000000000..bd6afbdcdc1 --- /dev/null +++ b/spec/components/previews/datepicker_preview/single.html.erb @@ -0,0 +1 @@ +<%= tag :'opce-single-date-picker', value: %> diff --git a/spec/components/previews/forms_preview.rb b/spec/components/previews/forms_preview.rb new file mode 100644 index 00000000000..d67e6c2947c --- /dev/null +++ b/spec/components/previews/forms_preview.rb @@ -0,0 +1,24 @@ +# @logical_path OpenProject +class FormsPreview < Lookbook::Preview + def default; end + + def bordered; end + + def bordered_compressed; end + + def collapsible; end + + def controls; end + + def field_types; end + + def field_widths; end + + def multiple_fields_per_row; end + + def sections_and_fieldsets; end + + def vertical_layout; end + + def wide_labels; end +end diff --git a/spec/components/previews/forms_preview/bordered.html.erb b/spec/components/previews/forms_preview/bordered.html.erb new file mode 100644 index 00000000000..fa68960d91f --- /dev/null +++ b/spec/components/previews/forms_preview/bordered.html.erb @@ -0,0 +1,14 @@ +
    +
    + +
    +
    + +
    +
    +
    + +
    + + +
    diff --git a/spec/components/previews/forms_preview/bordered_compressed.html.erb b/spec/components/previews/forms_preview/bordered_compressed.html.erb new file mode 100644 index 00000000000..389f61e263f --- /dev/null +++ b/spec/components/previews/forms_preview/bordered_compressed.html.erb @@ -0,0 +1,14 @@ +
    +
    + +
    +
    + +
    +
    +
    + +
    + + +
    diff --git a/spec/components/previews/forms_preview/collapsible.html.erb b/spec/components/previews/forms_preview/collapsible.html.erb new file mode 100644 index 00000000000..daea2e386bb --- /dev/null +++ b/spec/components/previews/forms_preview/collapsible.html.erb @@ -0,0 +1,28 @@ +
    +
    + + Less important information + + +
    +
    + + More important information + +
    + +
    +
    + +
    +
    +
    +
    +
    diff --git a/spec/components/previews/forms_preview/controls.html.erb b/spec/components/previews/forms_preview/controls.html.erb new file mode 100644 index 00000000000..7495a2e44e5 --- /dev/null +++ b/spec/components/previews/forms_preview/controls.html.erb @@ -0,0 +1,29 @@ +
    +
    + + Various information + +
    + + (Check all | Uncheck all) + +
    +
    + +
    + + +
    +
    +
    +
    diff --git a/spec/components/previews/forms_preview/default.html.erb b/spec/components/previews/forms_preview/default.html.erb new file mode 100644 index 00000000000..01fe4cea577 --- /dev/null +++ b/spec/components/previews/forms_preview/default.html.erb @@ -0,0 +1,71 @@ +
    +
    +
    + +
    +
    + +
    +
    +
    + Write anything you like. +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + + Add 5 + +
    +
    + Any number from 1 to 10! +
    +
    +
    + +
    +
    + +
    +
    +
    + Write more about anything. +
    +
    +
    +
    +
    + +
    +
    +
    + This field has no label, which means you really can write what you like. +
    +
    +
    +
    +
    + +
    +
    +
    + This field also has no label, but takes up the full width. +
    +
    +
    +
    diff --git a/spec/components/previews/forms_preview/field_types.html.erb b/spec/components/previews/forms_preview/field_types.html.erb new file mode 100644 index 00000000000..7ed22a8c0d3 --- /dev/null +++ b/spec/components/previews/forms_preview/field_types.html.erb @@ -0,0 +1,249 @@ +
    +

    Text fields

    + + + + +
    + +
    +

    Text areas

    + + + +
    + + +
    +

    Checkboxes

    + + + + + + +
    +
    + +
    +
    + +
    +
    +
    +
    + + +
    +
    + +
    + + + +
    +
    +
    +
    + + +
    +

    Radio buttons

    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    +

    Select fields

    + + + + + + + + + + +
    +
    + +
    +
    + +
    +
    +
    + Oranges are rich in Vitamin C. Eat more than two! +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + + +
    +

    Narrow select

    + +

    By default, a `form--select` will take the full width of its container element. + In most cases it is recommended to apply a width to the container element, but + in certain circumstances the `-narrow` variant may be preferable.

    + + + + + + +
    + + + +
    +

    Checkbox matrices

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    Attribute nameUser accessAdmin access
    + Project + + + + +
    + Type + + + + +
    + Parent + + + + +
    +
    + + +
    +

    Inline buttons

    + +
    +
    +
    + + + +
    +
    +
    +
    diff --git a/spec/components/previews/forms_preview/field_widths.html.erb b/spec/components/previews/forms_preview/field_widths.html.erb new file mode 100644 index 00000000000..466031dfbbd --- /dev/null +++ b/spec/components/previews/forms_preview/field_widths.html.erb @@ -0,0 +1,211 @@ +
    +
    +

    Input type "text-field"

    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + + +
    +

    Input type "text-area"

    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + + +
    +

    Input type "select"

    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    diff --git a/spec/components/previews/forms_preview/multiple_fields_per_row.html.erb b/spec/components/previews/forms_preview/multiple_fields_per_row.html.erb new file mode 100644 index 00000000000..de05d3f4ca5 --- /dev/null +++ b/spec/components/previews/forms_preview/multiple_fields_per_row.html.erb @@ -0,0 +1,119 @@ +
    +
    + Wichtige Daten +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + One never lies about one's age. +
    +
    +
    + +
    +
    + +
    +
    Liter
    +
    pro
    +
    + +
    +
    auf
    +
    + +
    +
    +
    +
    +
    Colors:
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    diff --git a/spec/components/previews/forms_preview/sections_and_fieldsets.html.erb b/spec/components/previews/forms_preview/sections_and_fieldsets.html.erb new file mode 100644 index 00000000000..941ab75846b --- /dev/null +++ b/spec/components/previews/forms_preview/sections_and_fieldsets.html.erb @@ -0,0 +1,36 @@ +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +

    Advanced information

    +
    + +
    +
    + +
    +
    +
    +
    +
    + + Even more advanced information + +
    + +
    +
    + +
    +
    +
    +
    +
    diff --git a/spec/components/previews/forms_preview/vertical_layout.html.erb b/spec/components/previews/forms_preview/vertical_layout.html.erb new file mode 100644 index 00000000000..9870e0d5f41 --- /dev/null +++ b/spec/components/previews/forms_preview/vertical_layout.html.erb @@ -0,0 +1,55 @@ +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + Write anything you like. +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + unit +
    +
    +
    + Any number from 1 to 10! +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + Write more about anything. +
    +
    +
    +
    +
    diff --git a/spec/components/previews/forms_preview/wide_labels.html.erb b/spec/components/previews/forms_preview/wide_labels.html.erb new file mode 100644 index 00000000000..30293f58a6c --- /dev/null +++ b/spec/components/previews/forms_preview/wide_labels.html.erb @@ -0,0 +1,80 @@ +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + Your personal email address. +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + per item +
    +
    +
    + The more, the better +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + Write more about anything. +
    +
    +
    +
    + + +
    +
    + Selecting these option might be considered a dangerous operation. +
    +
    +
    +
    diff --git a/spec/components/previews/links_preview.rb b/spec/components/previews/links_preview.rb new file mode 100644 index 00000000000..88eb1291b3c --- /dev/null +++ b/spec/components/previews/links_preview.rb @@ -0,0 +1,4 @@ +# @logical_path OpenProject +class LinksPreview < Lookbook::Preview + def default; end +end diff --git a/frontend/src/global_styles/content/_links.lsg b/spec/components/previews/links_preview/default.html.erb similarity index 94% rename from frontend/src/global_styles/content/_links.lsg rename to spec/components/previews/links_preview/default.html.erb index 09821c9cbfb..4181d30d686 100644 --- a/frontend/src/global_styles/content/_links.lsg +++ b/spec/components/previews/links_preview/default.html.erb @@ -1,6 +1,3 @@ -# Links - -``` A normal link
    @@ -13,4 +10,3 @@     -``` diff --git a/spec/components/previews/loading_indicator_preview.rb b/spec/components/previews/loading_indicator_preview.rb new file mode 100644 index 00000000000..7d6f7cd583f --- /dev/null +++ b/spec/components/previews/loading_indicator_preview.rb @@ -0,0 +1,4 @@ +# @logical_path OpenProject +class LoadingIndicatorPreview < Lookbook::Preview + def default; end +end diff --git a/spec/components/previews/loading_indicator_preview/default.html.erb b/spec/components/previews/loading_indicator_preview/default.html.erb new file mode 100644 index 00000000000..85f228b7828 --- /dev/null +++ b/spec/components/previews/loading_indicator_preview/default.html.erb @@ -0,0 +1,8 @@ +
    + + + + + + +
    diff --git a/spec/components/previews/modal_preview.rb b/spec/components/previews/modal_preview.rb new file mode 100644 index 00000000000..33cbc03505d --- /dev/null +++ b/spec/components/previews/modal_preview.rb @@ -0,0 +1,7 @@ +# @logical_path OpenProject +# @display min_height 500px +class ModalPreview < Lookbook::Preview + def default; end + + def danger_zone; end +end diff --git a/spec/components/previews/modal_preview/danger_zone.html.erb b/spec/components/previews/modal_preview/danger_zone.html.erb new file mode 100644 index 00000000000..671da13faa0 --- /dev/null +++ b/spec/components/previews/modal_preview/danger_zone.html.erb @@ -0,0 +1,20 @@ +
    +
    +
    + + Confirm deletion of Work Package +
    +
    +
    +

    Are you sure you want to delete the following work package? +

    +

    ...

    +
    +
    +
    + + +
    +
    +
    +
    diff --git a/spec/components/previews/modal_preview/default.html.erb b/spec/components/previews/modal_preview/default.html.erb new file mode 100644 index 00000000000..5b102a5c1e0 --- /dev/null +++ b/spec/components/previews/modal_preview/default.html.erb @@ -0,0 +1,58 @@ +
    +
    +
    + Export +
    +
    + +
    +
    + + +
    +
    +
    +
    diff --git a/spec/components/previews/news_preview.rb b/spec/components/previews/news_preview.rb new file mode 100644 index 00000000000..80d80713465 --- /dev/null +++ b/spec/components/previews/news_preview.rb @@ -0,0 +1,6 @@ +# @logical_path OpenProject +class NewsPreview < Lookbook::Preview + def default; end + + def detailed_view; end +end diff --git a/spec/components/previews/news_preview/default.html.erb b/spec/components/previews/news_preview/default.html.erb new file mode 100644 index 00000000000..bf491daddb3 --- /dev/null +++ b/spec/components/previews/news_preview/default.html.erb @@ -0,0 +1,28 @@ +
    +

    + Avatar + More news +

    +

    Added by OpenProject Admin + 3 months ago +

    +
    +

    + Lorem ipsum dolor sit amet, choro atomorum dissentias sit ea, duo labores eleifend ei, pri habeo latine cu. +

    +
    + +

    + Avatar + News +

    +

    Added by OpenProject Admin + 7 months ago +

    +
    +

    + Lorem ipsum dolor sit amet, choro atomorum dissentias sit ea, duo labores eleifend ei, pri habeo latine cu. + Cu nam fabellas apeirian patrioque. Has cu indoctum consulatu, nam invidunt nominati id. +

    +
    +
    diff --git a/frontend/src/global_styles/content/_news.lsg b/spec/components/previews/news_preview/detailed_view.html.erb similarity index 63% rename from frontend/src/global_styles/content/_news.lsg rename to spec/components/previews/news_preview/detailed_view.html.erb index 22c1df6c235..a7c3c942eb6 100644 --- a/frontend/src/global_styles/content/_news.lsg +++ b/spec/components/previews/news_preview/detailed_view.html.erb @@ -1,41 +1,3 @@ -# News - -## News: Overview - -``` -
    -

    - Avatar - More news -

    -

    Added by OpenProject Admin - 3 months ago -

    -
    -

    - Lorem ipsum dolor sit amet, choro atomorum dissentias sit ea, duo labores eleifend ei, pri habeo latine cu. -

    -
    - -

    - Avatar - News -

    -

    Added by OpenProject Admin - 7 months ago -

    -
    -

    - Lorem ipsum dolor sit amet, choro atomorum dissentias sit ea, duo labores eleifend ei, pri habeo latine cu. - Cu nam fabellas apeirian patrioque. Has cu indoctum consulatu, nam invidunt nominati id. -

    -
    -
    -``` - -## News: Detailed view - -```
    @@ -98,4 +60,3 @@

    -``` diff --git a/spec/components/previews/pagination_preview.rb b/spec/components/previews/pagination_preview.rb new file mode 100644 index 00000000000..eccbba3cd2d --- /dev/null +++ b/spec/components/previews/pagination_preview.rb @@ -0,0 +1,4 @@ +# @logical_path OpenProject +class PaginationPreview < Lookbook::Preview + def default; end +end diff --git a/spec/components/previews/pagination_preview/default.html.erb b/spec/components/previews/pagination_preview/default.html.erb new file mode 100644 index 00000000000..cff32e77a76 --- /dev/null +++ b/spec/components/previews/pagination_preview/default.html.erb @@ -0,0 +1,27 @@ +
    + +
    + +
    +
    diff --git a/spec/components/previews/progress_bar_preview.rb b/spec/components/previews/progress_bar_preview.rb new file mode 100644 index 00000000000..da7b71eb370 --- /dev/null +++ b/spec/components/previews/progress_bar_preview.rb @@ -0,0 +1,4 @@ +# @logical_path OpenProject +class ProgressBarPreview < Lookbook::Preview + def default; end +end diff --git a/frontend/src/global_styles/content/_progress_bar.lsg b/spec/components/previews/progress_bar_preview/default.html.erb similarity index 91% rename from frontend/src/global_styles/content/_progress_bar.lsg rename to spec/components/previews/progress_bar_preview/default.html.erb index 8186dad47ce..23db8a1f77a 100644 --- a/frontend/src/global_styles/content/_progress_bar.lsg +++ b/spec/components/previews/progress_bar_preview/default.html.erb @@ -1,6 +1,3 @@ -# Progress Bar - -``` @@ -8,4 +5,3 @@ 60% Total progress -``` diff --git a/spec/components/previews/scrollable_tabs_preview.rb b/spec/components/previews/scrollable_tabs_preview.rb new file mode 100644 index 00000000000..83fdfbd789e --- /dev/null +++ b/spec/components/previews/scrollable_tabs_preview.rb @@ -0,0 +1,10 @@ +# @logical_path OpenProject +class ScrollableTabsPreview < Lookbook::Preview + def default; end + + def no_ellipse; end + + def footer; end + + def no_content; end +end diff --git a/spec/components/previews/scrollable_tabs_preview/default.html.erb b/spec/components/previews/scrollable_tabs_preview/default.html.erb new file mode 100644 index 00000000000..17d8d44a454 --- /dev/null +++ b/spec/components/previews/scrollable_tabs_preview/default.html.erb @@ -0,0 +1,21 @@ +
    +
    + + + +
    +
    diff --git a/spec/components/previews/simple_filters_preview.rb b/spec/components/previews/simple_filters_preview.rb new file mode 100644 index 00000000000..426940a6f06 --- /dev/null +++ b/spec/components/previews/simple_filters_preview.rb @@ -0,0 +1,12 @@ +# @logical_path OpenProject +class SimpleFiltersPreview < Lookbook::Preview + # Simple filters + # -------------- + # Simple filters are forms that serve the purpose of limiting the number of entries in a list. + # As opposed to advanced filters however, there is no operator selection to search with. + # + # By default, simple filters can have multiple fields per row (as many as the given space allows). + def default; end + + def with_radio_buttons; end +end diff --git a/spec/components/previews/simple_filters_preview/default.html.erb b/spec/components/previews/simple_filters_preview/default.html.erb new file mode 100644 index 00000000000..3adbd5f391c --- /dev/null +++ b/spec/components/previews/simple_filters_preview/default.html.erb @@ -0,0 +1,36 @@ +
    + + Simple Filters +
      +
    • + +
      + +
      +
    • +
    • + +
      + +
      +
    • +
    • + +
      + +
      +
    • +
    • + Apply + Clear +
    • +
    +
    diff --git a/frontend/src/global_styles/content/_simple_filters.lsg b/spec/components/previews/simple_filters_preview/with_radio_buttons.html.erb similarity index 52% rename from frontend/src/global_styles/content/_simple_filters.lsg rename to spec/components/previews/simple_filters_preview/with_radio_buttons.html.erb index 13b0182fa3a..7f3b5e34966 100644 --- a/frontend/src/global_styles/content/_simple_filters.lsg +++ b/spec/components/previews/simple_filters_preview/with_radio_buttons.html.erb @@ -1,57 +1,3 @@ -# Simple filters - -Simple filters are forms that serve the purpose of limiting the number of entries in a list. As opposed to advanced filters however, there is no operator selection to search with. - -By default, simple filters can have multiple fields per row (as many as the given space allows). - -## Simple filters: Standard style - -``` -@full-width - -
    - - Simple Filters -
      -
    • - -
      - -
      -
    • -
    • - -
      - -
      -
    • -
    • - -
      - -
      -
    • -
    • - Apply - Clear -
    • -
    -
    -``` - -## Simple filters: With radio buttons - -``` -@full-width -
    Simple Filters @@ -101,4 +47,3 @@ By default, simple filters can have multiple fields per row (as many as the give
  • -``` diff --git a/spec/components/previews/table_preview.rb b/spec/components/previews/table_preview.rb new file mode 100644 index 00000000000..1dd475ad508 --- /dev/null +++ b/spec/components/previews/table_preview.rb @@ -0,0 +1,10 @@ +# @logical_path OpenProject +class TablePreview < Lookbook::Preview + def default; end + + def no_ellipse; end + + def footer; end + + def no_content; end +end diff --git a/spec/components/previews/table_preview/buttons.html.erb b/spec/components/previews/table_preview/buttons.html.erb new file mode 100644 index 00000000000..0094b7dbbd9 --- /dev/null +++ b/spec/components/previews/table_preview/buttons.html.erb @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + + Email + +
    +
    +
    +
    +
    + Roles +
    +
    +
    +
    +
    + Status +
    +
    +
    +
    +
    + member@example.net + Memberactive + + +
    + verylongemail_example@example.com + Memberactive + + +
    + member@example.net + Adminactive + + +
    diff --git a/spec/components/previews/table_preview/default.html.erb b/spec/components/previews/table_preview/default.html.erb new file mode 100644 index 00000000000..b34b4d6cd5b --- /dev/null +++ b/spec/components/previews/table_preview/default.html.erb @@ -0,0 +1,100 @@ +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + + First + +
    +
    +
    +
    +
    + + Second + +
    +
    +
    +
    +
    + + Third + +
    +
    +
    +
    +
    + + Fourth + +
    +
    +
    +
    +
    + + Fifth + +
    +
    +
    Lorem ipsum dolor sit amet, consectetur adipiscing elitNullam a sem et metus congue placerat.Mauris ut augue viverra, consequat eros eu, maximus quam.Maecenas elementum orci a varius suscipit.Nunc molestie neque sit amet eros semper dapibus.
    Nullam a sem et metus congue placerat.Lorem ipsum dolor sit amet, consectetur adipiscing elitMauris ut augue viverra, consequat eros eu, maximus quam.Maecenas elementum orci a varius suscipit.Nunc molestie neque sit amet eros semper dapibus.
    Nullam a sem et metus congue placerat.Mauris ut augue viverra, consequat eros eu, maximus quam.Lorem ipsum dolor sit amet, consectetur adipiscing elitMaecenas elementum orci a varius suscipit.Nunc molestie neque sit amet eros semper dapibus.
    Nullam a sem et metus congue placerat.Mauris ut augue viverra, consequat eros eu, maximus quam.Maecenas elementum orci a varius suscipit.Lorem ipsum dolor sit amet, consectetur adipiscing elitNunc molestie neque sit amet eros semper dapibus.
    Nullam a sem et metus congue placerat.Mauris ut augue viverra, consequat eros eu, maximus quam.Maecenas elementum orci a varius suscipit.Lorem ipsum dolor sit amet, consectetur adipiscing elitNunc molestie neque sit amet eros semper dapibus.
    + +
    +
    diff --git a/spec/components/previews/table_preview/footer.html.erb b/spec/components/previews/table_preview/footer.html.erb new file mode 100644 index 00000000000..170ad794843 --- /dev/null +++ b/spec/components/previews/table_preview/footer.html.erb @@ -0,0 +1,116 @@ + diff --git a/spec/components/previews/table_preview/no_content.html.erb b/spec/components/previews/table_preview/no_content.html.erb new file mode 100644 index 00000000000..b0ce07c496f --- /dev/null +++ b/spec/components/previews/table_preview/no_content.html.erb @@ -0,0 +1,11 @@ +
    +
    + + + Nothing to display + +
    +

    Either nothing have been created or everything is filtered out.

    +
    +
    +
    diff --git a/spec/components/previews/table_preview/no_ellipse.html.erb b/spec/components/previews/table_preview/no_ellipse.html.erb new file mode 100644 index 00000000000..e5855ba690b --- /dev/null +++ b/spec/components/previews/table_preview/no_ellipse.html.erb @@ -0,0 +1,74 @@ +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + + Content wraps in new lines + +
    +
    +
    +
    +
    + + clipped + +
    +
    +
    +
    +
    + + Third + +
    +
    +
    +
    +
    + + Fourth + +
    +
    +
    +
    +
    + + Fifth + +
    +
    +
    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus porta lorem congue lacus euismod egestas. Mauris odio orci, semper sed molestie viverra, vestibulum et nisi. + Nullam a sem et metus congue placerat. Text will overflow once max-width is exceededMauris ut augue viverra, consequat eros eu, maximus quam.Maecenas elementum orci a varius suscipit.Nunc molestie neque sit amet eros semper dapibus.
    + +
    +
    diff --git a/spec/components/previews/toast_preview.rb b/spec/components/previews/toast_preview.rb new file mode 100644 index 00000000000..09a52c56537 --- /dev/null +++ b/spec/components/previews/toast_preview.rb @@ -0,0 +1,12 @@ +# @logical_path OpenProject +class ToastPreview < Lookbook::Preview + def info; end + + def warning; end + + def error; end + + def success; end + + def upload; end +end diff --git a/spec/components/previews/toast_preview/error.html.erb b/spec/components/previews/toast_preview/error.html.erb new file mode 100644 index 00000000000..6488775a157 --- /dev/null +++ b/spec/components/previews/toast_preview/error.html.erb @@ -0,0 +1,12 @@ +
    + +
    +

    An error occurred, here are the facts:

    +
      +
    • Fact 1: You made a mistake
    • +
    • Fact 2: You really made a mistake
    • +
    • Fact 3: You should fix it
    • +
    + +
    +
    diff --git a/spec/components/previews/toast_preview/info.html.erb b/spec/components/previews/toast_preview/info.html.erb new file mode 100644 index 00000000000..355d3509f36 --- /dev/null +++ b/spec/components/previews/toast_preview/info.html.erb @@ -0,0 +1,5 @@ +
    +
    +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quia laudantium ea delectus incidunt accusantium repudiandae deserunt excepturi non esse vero distinctio et reprehenderit, cupiditate quidem consectetur rerum iste magnam voluptatibus.

    +
    +
    diff --git a/spec/components/previews/toast_preview/success.html.erb b/spec/components/previews/toast_preview/success.html.erb new file mode 100644 index 00000000000..a2e6292cb60 --- /dev/null +++ b/spec/components/previews/toast_preview/success.html.erb @@ -0,0 +1,6 @@ +
    + +
    +

    Successful update. A link to the past

    +
    +
    diff --git a/spec/components/previews/toast_preview/upload.html.erb b/spec/components/previews/toast_preview/upload.html.erb new file mode 100644 index 00000000000..156fa25a9a5 --- /dev/null +++ b/spec/components/previews/toast_preview/upload.html.erb @@ -0,0 +1,42 @@ +

    In progress

    +
    + +
    +
    + Uploading files... +
      + +
    • + Awesome_Landscape.png + 70% +
    • +
      +
    +
    +
    + +

    Uplaod progress done

    +
    + +
    +
    + Uploading files... +
    +
    +
      + +
    • + Awesome_Landscape.png + +
    • +
    • + Awesome_Landscape2.png + +

      Something went wrong!...

      +
    • +
      +
    +
    +
    +
    +
    diff --git a/spec/components/previews/toast_preview/warning.html.erb b/spec/components/previews/toast_preview/warning.html.erb new file mode 100644 index 00000000000..10b399e3782 --- /dev/null +++ b/spec/components/previews/toast_preview/warning.html.erb @@ -0,0 +1,6 @@ +
    + +
    +

    This is a warning. You may ignore it, but bad things might happen.

    +
    +
    diff --git a/spec/components/previews/toolbar_component_preview.rb b/spec/components/previews/toolbar_component_preview.rb new file mode 100644 index 00000000000..e0b04266598 --- /dev/null +++ b/spec/components/previews/toolbar_component_preview.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# @logical_path OpenProject +class ToolbarComponentPreview < Lookbook::Preview + # A toolbar that can and should be used for actions on the current view. + # Initially designed for the Work package list, this can be reused throughout the application. + def default; end + + def with_form_elements; end + + def with_labelled_form_elements; end +end diff --git a/spec/components/previews/toolbar_component_preview/default.html.erb b/spec/components/previews/toolbar_component_preview/default.html.erb new file mode 100644 index 00000000000..635b2c7cb56 --- /dev/null +++ b/spec/components/previews/toolbar_component_preview/default.html.erb @@ -0,0 +1,21 @@ +
    +
    +
    +

    Title of the page

    +
    + +
    +
    diff --git a/spec/components/previews/toolbar_component_preview/with_form_elements.html.erb b/spec/components/previews/toolbar_component_preview/with_form_elements.html.erb new file mode 100644 index 00000000000..d206baf2600 --- /dev/null +++ b/spec/components/previews/toolbar_component_preview/with_form_elements.html.erb @@ -0,0 +1,38 @@ +
    +
    +
    +

    Dragonball Z characters

    +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + + + Add + +
    • +
    +
    +

    now with extremely long subtitle: Lorem ipsum dolor sit amet, consectetur adipisicing elit. Iste consequatur doloribus suscipit nemo temporibus deserunt alias incidunt doloremque officia rerum, nobis fuga, recusandae voluptatibus voluptatem tenetur repellendus itaque et. Eum.

    +
    diff --git a/spec/components/previews/toolbar_component_preview/with_labelled_form_elements.html.erb b/spec/components/previews/toolbar_component_preview/with_labelled_form_elements.html.erb new file mode 100644 index 00000000000..982dd29c8c1 --- /dev/null +++ b/spec/components/previews/toolbar_component_preview/with_labelled_form_elements.html.erb @@ -0,0 +1,36 @@ +
    +
    +
    +

    Dragonball Z characters

    +
    +
      +
    • +
      + +
      + +
    • +
    • +
      + +
      + +
    • +
    +
    +

    now with extremely long subtitle: Lorem ipsum dolor sit amet, consectetur adipisicing elit. Iste consequatur doloribus suscipit nemo temporibus deserunt alias incidunt doloremque officia rerum, nobis fuga, recusandae voluptatibus voluptatem tenetur repellendus itaque et. Eum.

    +
    diff --git a/spec/components/previews/tooltip_component_preview/default.html.erb b/spec/components/previews/tooltip_component_preview/default.html.erb new file mode 100644 index 00000000000..493fa633b41 --- /dev/null +++ b/spec/components/previews/tooltip_component_preview/default.html.erb @@ -0,0 +1,11 @@ +<%= render TooltipComponent.new do |component| %> + <% component.with_trigger do %> + Hover me to see the tooltip + <% end %> + + <% component.with_body do %> + + Content of the tooltip + + <% end %> +<% end %> diff --git a/spec/components/previews/tooltip_preview.rb b/spec/components/previews/tooltip_preview.rb new file mode 100644 index 00000000000..46586aaea68 --- /dev/null +++ b/spec/components/previews/tooltip_preview.rb @@ -0,0 +1,12 @@ +# @logical_path OpenProject +class TooltipPreview < Lookbook::Preview + # These can contain simple texts. + # + # Attention: + # - They are not suitable for HTML within the Tooltip. + # - Also, if the are already :before or :after CSS rules for the decorated element, + # things will break as these rules will get overwritten. + def default; end + + def forms; end +end diff --git a/spec/components/previews/tooltip_preview/default.html.erb b/spec/components/previews/tooltip_preview/default.html.erb new file mode 100644 index 00000000000..b851332ffac --- /dev/null +++ b/spec/components/previews/tooltip_preview/default.html.erb @@ -0,0 +1,42 @@ +
    +
    + Right + + + +
    + +
    + Bottom + + + +
    + +
    + Left + + + +
    + +
    + Top + + + +
    +
    + + diff --git a/spec/components/previews/tooltip_preview/forms.html.erb b/spec/components/previews/tooltip_preview/forms.html.erb new file mode 100644 index 00000000000..43ffec07f60 --- /dev/null +++ b/spec/components/previews/tooltip_preview/forms.html.erb @@ -0,0 +1,60 @@ +
    +
    + +
    +
    + +
    +
    + + + +
    +
    +
    +
    + +
    +
    + +
    +
    + + + +
    +
    +
    +
    + +
    +
    + +
    +
    + + + +
    +
    +
    +
    + +
    +
    + +
    +
    + + + +
    +
    +
    +
    diff --git a/spec/components/previews/widget_component_preview.rb b/spec/components/previews/widget_component_preview.rb new file mode 100644 index 00000000000..a2f6238a21b --- /dev/null +++ b/spec/components/previews/widget_component_preview.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# @logical_path OpenProject +# @label Widget boxes +class WidgetComponentPreview < Lookbook::Preview + def default; end +end diff --git a/spec/components/previews/widget_component_preview/default.html.erb b/spec/components/previews/widget_component_preview/default.html.erb new file mode 100644 index 00000000000..47089ba6af5 --- /dev/null +++ b/spec/components/previews/widget_component_preview/default.html.erb @@ -0,0 +1,30 @@ +
    +
    +

    + Delete account foo.bar@openproject.org +

    +

    + Your account will be deleted from the system. Therefore, you will no longer be able to log in with your current credentials. If you choose to become a user of this application again, you can do so by using the means this application grants. +

    +

    + Of the data you created as much as possible will be deleted. Note however, that data like work packages and wiki entries can not be deleted without impeding the work of the other users. Such data is hence reassigned to an account called "Deleted user". +

    +

    + + Deleting your account is an irreversible action. +

    +

    + Enter your login foo.bar@openproject.org to verify the deletion. +

    +
    + + + + Cancel + +
    +
    +
    diff --git a/spec/controllers/ldap_auth_sources_controller_spec.rb b/spec/controllers/ldap_auth_sources_controller_spec.rb index 6c746100548..e19a5efbf7e 100644 --- a/spec/controllers/ldap_auth_sources_controller_spec.rb +++ b/spec/controllers/ldap_auth_sources_controller_spec.rb @@ -41,13 +41,13 @@ RSpec.describe LdapAuthSourcesController do get :new end - it { expect(assigns(:auth_source)).not_to be_nil } + it { expect(assigns(:ldap_auth_source)).not_to be_nil } it { is_expected.to respond_with :success } it { is_expected.to render_template :new } it 'initializes a new LdapAuthSource' do - expect(assigns(:auth_source).class).to eq LdapAuthSource - expect(assigns(:auth_source)).to be_new_record + expect(assigns(:ldap_auth_source).class).to eq LdapAuthSource + expect(assigns(:ldap_auth_source)).to be_new_record end end @@ -77,7 +77,7 @@ RSpec.describe LdapAuthSourcesController do get :edit, params: { id: ldap.id } end - it { expect(assigns(:auth_source)).to eq ldap } + it { expect(assigns(:ldap_auth_source)).to eq ldap } it { is_expected.to respond_with :success } it { is_expected.to render_template :edit } end diff --git a/spec/controllers/my_controller_spec.rb b/spec/controllers/my_controller_spec.rb index 808223bbb34..d962f805b5f 100644 --- a/spec/controllers/my_controller_spec.rb +++ b/spec/controllers/my_controller_spec.rb @@ -345,5 +345,29 @@ RSpec.describe MyController do end end end + + describe 'file storage' do + let(:client) { create(:oauth_client, integration: create(:nextcloud_storage)) } + let(:token) { create(:oauth_client_token, oauth_client: client, scope: nil, user:, expires_in: 3_600) } + + render_views + + before { token } + + it 'list the tokens' do + get :access_token + expect(response.body).to have_selector("#storage-oauth-token-#{token.id}") + end + + it 'can remove the token' do + expect do + delete :delete_storage_token, params: { id: token.id } + end.to change(OAuthClientToken, :count).by(-1) + + expect(flash[:info]).to be_present + expect(flash[:error]).not_to be_present + expect(response).to redirect_to(action: :access_token) + end + end end end diff --git a/spec/controllers/roles_controller_spec.rb b/spec/controllers/roles_controller_spec.rb index 387e5d6a9e4..96592a8f719 100644 --- a/spec/controllers/roles_controller_spec.rb +++ b/spec/controllers/roles_controller_spec.rb @@ -361,7 +361,7 @@ RSpec.describe RolesController do it 'has the service call assigned' do expect(assigns[:calls]) - .to match_array [service_call0, service_call1, service_call2, service_call3] + .to contain_exactly(service_call0, service_call1, service_call2, service_call3) end it 'has the roles assigned' do @@ -371,55 +371,41 @@ RSpec.describe RolesController do end end - describe '#destroys' do - let(:role) do - build_stubbed(:role).tap do |d| - allow(Role) - .to receive(:find) - .with(d.id.to_s) - .and_return(d) - - allow(d) - .to receive(:destroy) - end - end + describe '#destroy' do + let(:role) { create(:role) } let(:params) { { id: role.id } } - subject do - delete :destroy, params: - end + subject { delete(:destroy, params:) } context 'with the role not in use' do - before { subject } + it 'redirects and destroyes the role' do + allow_any_instance_of(Role).to receive(:permissions).and_return([:read_files]) + role + expect(Role.count).to eq(1) + expect(enqueued_jobs.count).to eq(0) - it 'redirects' do - expect(response) - .to redirect_to roles_path - end + subject - it 'destroys the role' do - expect(role) - .to have_received(:destroy) + expect(enqueued_jobs.count).to eq(1) + expect(enqueued_jobs[0][:job]).to eq(Storages::ManageNextcloudIntegrationEventsJob) + expect(response).to redirect_to roles_path + expect(Role.count).to eq(0) end end context 'with the role in use' do - before do - allow(role) - .to receive(:destroy) - .and_throw(:an_error) + it 'redirects with a flash error' do + allow_any_instance_of(Role).to receive(:deletable?).and_return(false) + role + expect(Role.count).to eq(1) + expect(enqueued_jobs.count).to eq(0) subject - end - it 'redirects' do - expect(response) - .to redirect_to roles_path - end - - it 'sets a flash error' do - expect(flash[:error]) - .to eq I18n.t(:error_can_not_remove_role) + expect(enqueued_jobs.count).to eq(0) + expect(Role.count).to eq(1) + expect(response).to redirect_to roles_path + expect(flash[:error]).to eq I18n.t(:error_can_not_remove_role) end end end diff --git a/spec/factories/custom_field_factory.rb b/spec/factories/custom_field_factory.rb index df68f3500d1..c124f9fdd16 100644 --- a/spec/factories/custom_field_factory.rb +++ b/spec/factories/custom_field_factory.rb @@ -182,6 +182,21 @@ FactoryBot.define do type { 'WorkPackageCustomField' } is_filter { true } + transient do + projects { [] } + end + + after(:create) do |custom_field, evaluator| + evaluator.projects.each do |project| + project.work_package_custom_fields << custom_field + end + + if evaluator.projects.any? || evaluator.types.any? + # As the request store keeps track of the available custom fields of work packages + RequestStore.clear! + end + end + factory :list_wp_custom_field do sequence(:name) { |n| "List CF #{n}" } field_format { 'list' } diff --git a/spec/factories/ldap_auth_source_factory.rb b/spec/factories/ldap_auth_source_factory.rb index 976bf01b0e8..e782d441af4 100644 --- a/spec/factories/ldap_auth_source_factory.rb +++ b/spec/factories/ldap_auth_source_factory.rb @@ -33,5 +33,6 @@ FactoryBot.define do port { 225 } # a reserved port, should not be in use attr_login { 'uid' } tls_mode { 'plain_ldap' } + base_dn { 'dc=example,dc=com' } end end diff --git a/spec/factories/work_package_factory.rb b/spec/factories/work_package_factory.rb index 29d06e8c61e..8e3721d2fd9 100644 --- a/spec/factories/work_package_factory.rb +++ b/spec/factories/work_package_factory.rb @@ -78,8 +78,10 @@ FactoryBot.define do custom_values = evaluator.custom_values || {} if custom_values.is_a? Hash - custom_values.each_pair do |custom_field_id, value| - work_package.custom_values.build custom_field_id:, value: + custom_values.each_pair do |custom_field_id, values| + Array(values).each do |value| + work_package.custom_values.build custom_field_id:, value: + end end else custom_values.each { |cv| work_package.custom_values << cv } diff --git a/spec/features/activities/project_attributes_activity_spec.rb b/spec/features/activities/project_attributes_activity_spec.rb index 68507def74e..26e9fd9abad 100644 --- a/spec/features/activities/project_attributes_activity_spec.rb +++ b/spec/features/activities/project_attributes_activity_spec.rb @@ -29,16 +29,9 @@ require 'spec_helper' RSpec.describe 'Project attributes activity', :js, :with_cuprite do - let(:user) do - create(:user, - member_in_project: project, - member_with_permissions: %w[view_wiki_pages - edit_wiki_pages - view_wiki_edits]) - end - + let(:user) { create(:user, member_in_project: project) } let(:parent_project) { create(:project, name: 'parent') } - let(:project) { create(:project, parent: parent_project, active: false, enabled_module_names: %w[activity]) } + let(:project) { create(:project, parent: parent_project, active: false) } let!(:list_project_custom_field) { create(:list_project_custom_field) } let!(:version_project_custom_field) { create(:version_project_custom_field) } @@ -50,6 +43,7 @@ RSpec.describe 'Project attributes activity', :js, :with_cuprite do let!(:string_project_custom_field) { create(:string_project_custom_field) } let!(:date_project_custom_field) { create(:date_project_custom_field) } + let(:old_version) { create(:version, project:, name: 'Ringbo 1.0') } let(:next_version) { create(:version, project:, name: 'Turfu 2.0') } current_user { user } @@ -66,7 +60,7 @@ RSpec.describe 'Project attributes activity', :js, :with_cuprite do active: true, templated: true, list_project_custom_field.attribute_name => list_project_custom_field.possible_values.first.id, - version_project_custom_field.attribute_name => next_version.id, + version_project_custom_field.attribute_name => [old_version.id, next_version.id], bool_project_custom_field.attribute_name => true, user_project_custom_field.attribute_name => current_user.id, int_project_custom_field.attribute_name => 42, @@ -108,7 +102,8 @@ RSpec.describe 'Project attributes activity', :js, :with_cuprite do # custom fields expect(page).to have_selector('li', text: "#{list_project_custom_field.name} " \ "set to #{project.send(list_project_custom_field.attribute_getter)}") - expect(page).to have_selector('li', text: "#{version_project_custom_field.name} set to #{next_version.name}") + expect(page).to have_selector('li', text: "#{version_project_custom_field.name} " \ + "set to #{old_version.name}, #{next_version.name}") expect(page).to have_selector('li', text: "#{bool_project_custom_field.name} set to Yes") expect(page).to have_selector('li', text: "#{user_project_custom_field.name} set to #{current_user.name}") expect(page).to have_selector('li', text: "#{int_project_custom_field.name} set to 42") diff --git a/spec/features/custom_fields/multi_version_custom_field_spec.rb b/spec/features/custom_fields/multi_version_custom_field_spec.rb new file mode 100644 index 00000000000..a7cbcba004a --- /dev/null +++ b/spec/features/custom_fields/multi_version_custom_field_spec.rb @@ -0,0 +1,116 @@ +require "spec_helper" +require "support/pages/work_packages/abstract_work_package" + +RSpec.describe "multi version custom field", js: true do + shared_let(:admin) { create(:admin) } + let(:current_user) { admin } + let(:wp_page) { Pages::FullWorkPackage.new work_package } + let(:cf_edit_field) do + field = wp_page.edit_field custom_field.attribute_name(:camel_case) + field.field_type = 'create-autocompleter' + field + end + let(:work_package) { create(:work_package, project:, type:) } + let!(:version_old) { create(:version, project:, name: 'Version Old') } + let!(:version_current) { create(:version, project:, name: 'Version Current') } + let!(:version_future) { create(:version, project:, name: 'Version Future') } + + shared_let(:type) { create(:type) } + shared_let(:project) { create(:project, types: [type]) } + shared_let(:role) { create(:role) } + + shared_let(:custom_field) do + create( + :version_wp_custom_field, + name: "Fix version", + multi_value: true, + types: [type], + projects: [project] + ) + end + + before do + login_as current_user + wp_page.visit! + wp_page.ensure_page_loaded + end + + it "is shown and allowed to be updated" do + expect(page).to have_text custom_field.name + + cf_edit_field.activate! + cf_edit_field.set_value "Version Old" + cf_edit_field.set_value "Version Current" + cf_edit_field.set_value "Version Future" + + cf_edit_field.submit_by_dashboard + + expect(page).to have_text custom_field.name + expect(page).to have_text "Version Old" + expect(page).to have_text "Version Current" + expect(page).to have_text "Version Future" + + wp_page.expect_and_dismiss_toaster(message: "Successful update.") + + work_package.reload + cvs = work_package + .custom_value_for(custom_field) + .map(&:typed_value) + + expect(cvs).to contain_exactly(version_old, version_current, version_future) + + cf_edit_field.activate! + cf_edit_field.unset_value "Version Current", multi: true + cf_edit_field.unset_value "Version Future", multi: true + cf_edit_field.submit_by_dashboard + + wp_page.expect_and_dismiss_toaster(message: "Successful update.") + + expect(page).to have_text "Version Old" + expect(page).not_to have_text "Version Current" + expect(page).not_to have_text "Version Future" + + work_package.reload + + # only one value, so no array + cvs = work_package + .custom_value_for(custom_field) + .typed_value + + expect(cvs).to eq version_old + end + + context "with existing version values" do + let(:work_package) do + wp = build(:work_package, project:, type:) + + wp.custom_field_values = { + custom_field.id => [version_old.id.to_s, version_current.id.to_s] + } + + wp.save + wp + end + + it "is shown and allowed to be updated" do + expect(page).to have_text custom_field.name + expect(page).to have_text "Version Old" + expect(page).to have_text "Version Current" + + page.find(".inline-edit--display-field", text: "Version Old").click + + cf_edit_field.unset_value "Version Old", multi: true + cf_edit_field.set_value "Version Future" + + click_on "Fix version: Save" + wp_page.expect_and_dismiss_toaster(message: "Successful update.") + # .customField above is required to ignore Assignee and Accountable which are not interesting for us. + expect(page).to have_selector(".customField#{custom_field.id} .custom-option", count: 2) + + expect(page).to have_text custom_field.name + expect(page).to have_text "Version Current" + expect(page).to have_text "Version Future" + expect(page).not_to have_text "Version Old" + end + end +end diff --git a/spec/features/ldap_auth_sources/crud_spec.rb b/spec/features/ldap_auth_sources/crud_spec.rb index 3c1125456ab..f5ca3d7ac61 100644 --- a/spec/features/ldap_auth_sources/crud_spec.rb +++ b/spec/features/ldap_auth_sources/crud_spec.rb @@ -95,4 +95,27 @@ RSpec.describe 'CRUD LDAP connections', ldap_page.expect_and_dismiss_toaster message: 'Successful update.' expect(page).to have_selector('td.name', text: 'Updated Admin connection') end + + context 'when providing seed variables', + :settings_reset, + with_env: { + OPENPROJECT_SEED_LDAP_FOOBAR_HOST: "localhost" + } do + let!(:ldap_auth_source) { create(:ldap_auth_source, name: 'foobar') } + + it 'blocks editing of that connection by name' do + reset(:seed_ldap) + + ldap_page.visit! + expect(page).to have_text 'foobar' + + page.within("#ldap-auth-source-#{ldap_auth_source.id}") do + click_on 'foobar' + end + + expect(page).to have_text(I18n.t(:label_seeded_from_env_warning)) + expect(page).to have_field('ldap_auth_source_name', with: 'foobar', disabled: true) + expect(page).not_to have_button 'Save' + end + end end diff --git a/spec/features/projects/copy_spec.rb b/spec/features/projects/copy_spec.rb index 6d024c1b3cf..3d890ea6917 100644 --- a/spec/features/projects/copy_spec.rb +++ b/spec/features/projects/copy_spec.rb @@ -29,9 +29,7 @@ require 'spec_helper' # rubocop:disable RSpec:MultipleMemoizedHelpers -RSpec.describe 'Projects copy', - js: true, - with_cuprite: true do +RSpec.describe 'Projects copy', :with_cuprite, js: true do describe 'with a full copy example' do let!(:project) do create(:project, @@ -66,7 +64,7 @@ RSpec.describe 'Projects copy', create(:text_wp_custom_field) end let(:active_types) do - [create(:type), create(:type)] + create_list(:type, 2) end let!(:inactive_type) do create(:type) @@ -77,7 +75,15 @@ RSpec.describe 'Projects copy', permissions:) end let(:permissions) do - %i(copy_projects edit_project add_subprojects manage_types view_work_packages select_custom_fields work_package_assigned) + %i(copy_projects + edit_project + add_subprojects + manage_types + view_work_packages + select_custom_fields + manage_storages_in_project + manage_file_links + work_package_assigned) end let(:wp_user) do user = create(:user) @@ -162,8 +168,7 @@ RSpec.describe 'Projects copy', copied_project = Project.find_by(name: 'Copied project') - expect(copied_project) - .to be_present + expect(copied_project).to be_present # Will redirect to the new project automatically once the copy process is done expect(page).to have_current_path(Regexp.new("#{project_path(copied_project)}/?")) @@ -202,49 +207,32 @@ RSpec.describe 'Projects copy', .to eq ['wiki_page_attachment.pdf'] # Expect ProjectStores and their FileLinks were copied - expect(copied_project.projects_storages.count).to eq(project.projects_storages.count) + expect(copied_project.project_storages.count).to eq(project.project_storages.count) expect(copied_project.work_packages[0].file_links.count).to eq(project.work_packages[0].file_links.count) # custom field is copied over where the author is the current user # Using the db directly due to performance and clarity copied_work_packages = copied_project.work_packages - expect(copied_work_packages.length) - .to eql 1 + expect(copied_work_packages.length).to eql 1 copied_work_package = copied_work_packages[0] - expect(copied_work_package.subject) - .to eql work_package.subject - expect(copied_work_package.author) - .to eql user - expect(copied_work_package.assigned_to) - .to eql work_package.assigned_to - expect(copied_work_package.responsible) - .to eql work_package.responsible - expect(copied_work_package.status) - .to eql work_package.status - expect(copied_work_package.done_ratio) - .to eql work_package.done_ratio - expect(copied_work_package.description) - .to eql work_package.description - expect(copied_work_package.category) - .to eql copied_project.categories.find_by(name: category.name) - expect(copied_work_package.version) - .to eql copied_project.versions.find_by(name: version.name) - expect(copied_work_package.custom_value_attributes) - .to eql(wp_custom_field.id => 'Some wp cf text') - expect(copied_work_package.attachments.map(&:filename)) - .to eq ['work_package_attachment.pdf'] + expect(copied_work_package.subject).to eql work_package.subject + expect(copied_work_package.author).to eql user + expect(copied_work_package.assigned_to).to eql work_package.assigned_to + expect(copied_work_package.responsible).to eql work_package.responsible + expect(copied_work_package.status).to eql work_package.status + expect(copied_work_package.done_ratio).to eql work_package.done_ratio + expect(copied_work_package.description).to eql work_package.description + expect(copied_work_package.category).to eql copied_project.categories.find_by(name: category.name) + expect(copied_work_package.version).to eql copied_project.versions.find_by(name: version.name) + expect(copied_work_package.custom_value_attributes).to eql(wp_custom_field.id => 'Some wp cf text') + expect(copied_work_package.attachments.map(&:filename)).to eq ['work_package_attachment.pdf'] - expect(ActionMailer::Base.deliveries.count) - .to eql(1) - - expect(ActionMailer::Base.deliveries.last.subject) - .to eql("Created project Copied project") - - expect(ActionMailer::Base.deliveries.last.to) - .to match_array([user.mail]) + expect(ActionMailer::Base.deliveries.count).to eql(1) + expect(ActionMailer::Base.deliveries.last.subject).to eql("Created project Copied project") + expect(ActionMailer::Base.deliveries.last.to).to match_array([user.mail]) end end diff --git a/spec/features/work_packages/table/baseline/baseline_query_spec.rb b/spec/features/work_packages/table/baseline/baseline_query_spec.rb index aa1ff7efa34..54bb52c9890 100644 --- a/spec/features/work_packages/table/baseline/baseline_query_spec.rb +++ b/spec/features/work_packages/table/baseline/baseline_query_spec.rb @@ -29,7 +29,8 @@ require 'spec_helper' RSpec.describe 'baseline query saving', - js: true, + :js, + :with_cuprite, with_ee: %i[baseline_comparison], with_settings: { date_format: '%Y-%m-%d' } do shared_let(:project) { create(:project) } @@ -195,7 +196,7 @@ RSpec.describe 'baseline query saving', login_as tokyo_user wp_table.visit_query query - baseline.expect_legend_text "Changes since 2023-05-19 8:00 AM UTC+2 - 2023-05-25 8:00 PM UTC+2" + baseline.expect_legend_text "Changes between 2023-05-19 8:00 AM UTC+2 and 2023-05-25 8:00 PM UTC+2" baseline.expect_legend_tooltip "In your local timezone: 2023-05-19 3:00 PM UTC+9 - 2023-05-26 3:00 AM UTC+9" baseline_modal.expect_closed diff --git a/spec/features/work_packages/table/baseline/baseline_rendering_spec.rb b/spec/features/work_packages/table/baseline/baseline_rendering_spec.rb index e56b8f50f6b..92a5395011d 100644 --- a/spec/features/work_packages/table/baseline/baseline_rendering_spec.rb +++ b/spec/features/work_packages/table/baseline/baseline_rendering_spec.rb @@ -29,14 +29,44 @@ require 'spec_helper' RSpec.describe 'baseline rendering', - js: true, + :js, + :with_cuprite, with_settings: { date_format: '%Y-%m-%d' } do + shared_let(:list_wp_custom_field) { create(:list_wp_custom_field) } + shared_let(:multi_list_wp_custom_field) { create(:list_wp_custom_field, multi_value: true) } + shared_let(:version_wp_custom_field) { create(:version_wp_custom_field) } + shared_let(:bool_wp_custom_field) { create(:bool_wp_custom_field) } + shared_let(:user_wp_custom_field) { create(:user_wp_custom_field) } + shared_let(:int_wp_custom_field) { create(:int_wp_custom_field) } + shared_let(:float_wp_custom_field) { create(:float_wp_custom_field) } + shared_let(:string_wp_custom_field) { create(:string_wp_custom_field) } + shared_let(:date_wp_custom_field) { create(:date_wp_custom_field) } + + shared_let(:custom_fields) do + [ + list_wp_custom_field, + multi_list_wp_custom_field, + version_wp_custom_field, + bool_wp_custom_field, + user_wp_custom_field, + int_wp_custom_field, + float_wp_custom_field, + string_wp_custom_field, + date_wp_custom_field + ] + end + shared_let(:type_bug) { create(:type_bug) } - shared_let(:type_task) { create(:type_task) } + shared_let(:type_task) { create(:type_task, custom_fields:) } shared_let(:type_milestone) { create(:type_milestone) } - shared_let(:project) { create(:project, types: [type_bug, type_task, type_milestone]) } + + shared_let(:project) do + create(:project, + types: [type_bug, type_task, type_milestone], + work_package_custom_fields: custom_fields) + end shared_let(:user) do - create(:user, + create(:admin, firstname: 'Itsa', lastname: 'Me', member_in_project: project, @@ -101,8 +131,8 @@ RSpec.describe 'baseline rendering', .new(user:, model: wp) .call( subject: 'New subject', - start_date: Date.today - 1.day, - due_date: Date.today, + start_date: Time.zone.today - 1.day, + due_date: Time.zone.today, assigned_to: user, responsible: user, priority: high_priority, @@ -175,6 +205,56 @@ RSpec.describe 'baseline rendering', .result end + shared_let(:initial_custom_values) do + { + int_wp_custom_field.id => 1, + string_wp_custom_field.id => 'this is a string', + bool_wp_custom_field.id => true, + float_wp_custom_field.id => nil, + date_wp_custom_field.id => Date.yesterday, + list_wp_custom_field.id => nil, + multi_list_wp_custom_field.id => multi_list_wp_custom_field.possible_values, # not working + user_wp_custom_field.id => [assignee.id.to_s], + version_wp_custom_field.id => version_a + } + end + + shared_let(:changed_custom_values) do + { + "custom_field_#{int_wp_custom_field.id}": nil, + "custom_field_#{string_wp_custom_field.id}": 'this is a changed string', + "custom_field_#{bool_wp_custom_field.id}": false, + "custom_field_#{float_wp_custom_field.id}": 3.7, + "custom_field_#{date_wp_custom_field.id}": Time.zone.today, + "custom_field_#{list_wp_custom_field.id}": [list_wp_custom_field.possible_values.second], + "custom_field_#{multi_list_wp_custom_field.id}": multi_list_wp_custom_field.possible_values.take(2), + "custom_field_#{user_wp_custom_field.id}": nil, + "custom_field_#{version_wp_custom_field.id}": version_b + } + end + + shared_let(:wp_task_cf) do + wp = Timecop.travel(5.days.ago) do + # We do not have a logged in user and setting a user id for the custom_values user field + # fails validation, because the AnonymousUser does not have visibility to any users. + # To solve the issue, we create the work package as `user`. + User.execute_as user do + create(:work_package, + :skip_validations, + project:, + type: type_task, + subject: 'A task with CFs', + custom_values: initial_custom_values) + end + end + + WorkPackages::UpdateService + .new(user:, model: wp) + .call(changed_custom_values) + .on_failure { |result| raise result.message } + .result + end + shared_let(:query) do query = create(:query, name: 'Timestamps Query', @@ -183,7 +263,9 @@ RSpec.describe 'baseline rendering', query.timestamps = ["P-2d", "PT0S"] query.add_filter('type_id', '=', [type_task.id, type_milestone.id]) - query.column_names = %w[id subject status type start_date due_date version priority assigned_to responsible] + query.column_names = + %w[id subject status type start_date due_date version priority assigned_to responsible] + + CustomField.all.pluck(:id).map { |id| "cf_#{id}" } query.save!(validate: false) query @@ -243,6 +325,45 @@ RSpec.describe 'baseline rendering', baseline.expect_unchanged_attributes wp_task, :type, :subject, :start_date, :due_date, :version, :priority, :assignee, :accountable + + baseline.expect_changed_attributes wp_task_cf, + "customField#{int_wp_custom_field.id}": [ + '1', + '-' + ], + "customField#{string_wp_custom_field.id}": [ + 'this is a string', + 'this is a changed string' + ], + "customField#{bool_wp_custom_field.id}": [ + 'yes', + 'no' + ], + "customField#{float_wp_custom_field.id}": [ + '-', + '3.7' + ], + "customField#{date_wp_custom_field.id}": [ + Date.yesterday.iso8601, + Time.zone.today.iso8601 + ], + "customField#{list_wp_custom_field.id}": [ + "-", + "B" + ], + "customField#{multi_list_wp_custom_field.id}": [ + "A, B, ...7", + "A, B" + ], + "customField#{user_wp_custom_field.id}": [ + 'Assigned User', + '-' + ], + "customField#{version_wp_custom_field.id}": [ + 'Version A', + 'Version B' + ] + # show icons on work package single card display_representation.switch_to_card_layout within "wp-single-card[data-work-package-id='#{wp_bug_was_task.id}']" do @@ -258,6 +379,49 @@ RSpec.describe 'baseline rendering', expect(page).not_to have_selector(".op-wp-single-card--content-baseline") end end + + shared_examples_for 'selecting a builtin view' do + let(:builtin_view_name) { 'All open' } + + before do + within '#main-menu' do + click_link builtin_view_name + end + end + + it 'does not show changes or render baseline details' do + wp_table.expect_title builtin_view_name + + baseline.expect_inactive + baseline.expect_no_legends + baseline_modal.toggle_drop_modal + baseline_modal.expect_selected '-' + end + end + + context 'when a baseline filter is set' do + before do + wp_table.visit! + + wait_for_reload # Ensure page is fully loaded + + baseline_modal.toggle_drop_modal + baseline_modal.select_filter 'yesterday' + baseline_modal.apply + end + + context 'and the query is saved' do + before do + wp_table.save_as 'My Baseline Query' + end + + it_behaves_like 'selecting a builtin view' + end + + context 'and the query is not saved' do + it_behaves_like 'selecting a builtin view' + end + end end describe 'with feature disabled', with_flag: { show_changes: false } do @@ -269,6 +433,7 @@ RSpec.describe 'baseline rendering', baseline.expect_inactive end end + describe 'without EE', with_ee: false, with_flag: { show_changes: true } do it 'disabled options' do wp_table.visit_query(query) diff --git a/spec/features/work_packages/table/edit_work_packages_spec.rb b/spec/features/work_packages/table/edit_work_packages_spec.rb index 4d8aa7b7c9b..371944c2488 100644 --- a/spec/features/work_packages/table/edit_work_packages_spec.rb +++ b/spec/features/work_packages/table/edit_work_packages_spec.rb @@ -116,28 +116,32 @@ RSpec.describe 'Inline editing work packages', js: true do end context 'custom field' do - let(:custom_fields) do + let!(:custom_fields) do fields = [ create( :work_package_custom_field, field_format: 'list', possible_values: %w(foo bar xyz), - is_required: true, - is_for_all: false + is_required: false, + is_for_all: false, + types: [type], + projects: [project] ), create( :work_package_custom_field, field_format: 'string', - is_required: true, - is_for_all: false + is_required: false, + is_for_all: false, + types: [type], + projects: [project] ) ] fields end - let(:type) { create(:type_task, custom_fields:) } + let(:type) { create(:type_task) } let(:project) { create(:project, types: [type]) } - let(:work_package) do + let!(:work_package) do create(:work_package, subject: 'Foobar', status: status1, @@ -146,12 +150,11 @@ RSpec.describe 'Inline editing work packages', js: true do end before do - work_package workflow - # Require custom fields for this project - project.work_package_custom_fields = custom_fields - project.save! + custom_fields.each do |custom_field| + custom_field.update_attribute(:is_required, true) + end wp_table.visit! wp_table.expect_work_package_listed(work_package) diff --git a/spec/fixtures/files/example.com.crt b/spec/fixtures/files/example.com.crt new file mode 100644 index 00000000000..fae66439427 --- /dev/null +++ b/spec/fixtures/files/example.com.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFOzCCAyOgAwIBAgIUNKLrEALGA1SQ3YvCTAyGkBKIfDwwDQYJKoZIhvcNAQEL +BQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjMwNzI3MTQxNzE5WhcNMzMw +NzI0MTQxNzE5WjAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAKuFN324z9TlT1aEyCnWRXj6SzturoQdM4B3/0AF +RO7nlHzoE9vahFVtrD1U9oKp2g3dInCjLkPBOioK79HqW8bQ2m1hgEzZ01/VLG8W +SKKUoGDNfeip3dnG96gdJfNIaR9ZtU7EVX3ihHOonj1aU5ScsqZqDt/aEhamAuC0 +V+k4Wnbw/YMJcIlrWBGgyE8ypmirZqJxd0LVJcCjU3rv4Y4Q/8xfjlPJQ3crJpIb +zlIKJb+lY2H8G2jdCcg2nT2dqqNmc22R/uHI78DATZvnskM5MoHJEXHS650LBdNB +lzxixPCWfxlLwwCmCRYUq2cfg4v//0Qs944iPklgdvv31SAnDN+4c6shNWaqeqAt +aVljRsTcNQ6obtY9iU+vKwIhf4eKQB/L+dXNef8FE+PvutRG0jaNrVYILPQBjtWA +mW++MA5O0LQLmUaqKsJ5d6hVG/SLqiYA5GYIP3AZ6aEfdw0O1I5kPki/ePsSA9nd +keP+5MbkegcZWd4+GX9C6tD+DU/DaSH0tl1xEDer33ROE+CUxapGD2UrXUgBOHC5 +o0fOp27/thPffQmq1F+dn4iPiirTUOS6uXpI/7QJ7hqyPz9FuHB6gewuckPS1LRu +THIQEcHOYgxiTzDqRgf8Td8ba73otuVXBlsmV4HMDt+5OH/LcySuyoMp8Knt5aeO +ksRvAgMBAAGjgYAwfjAdBgNVHQ4EFgQUJtpmQh+zH3F8+4IG1q7s9WK7S60wHwYD +VR0jBBgwFoAUJtpmQh+zH3F8+4IG1q7s9WK7S60wDwYDVR0TAQH/BAUwAwEB/zAr +BgNVHREEJDAiggtleGFtcGxlLmNvbYINKi5leGFtcGxlLmNvbYcECgAAATANBgkq +hkiG9w0BAQsFAAOCAgEAntYwbpIQBrg4JJV9dxKqEgQOwj6zSOkOF4RdBJT339zL +MDMZjWGtkB6qMC2agsHZLlPHwz+vtsJndsd4UzE5QyoI2zkbekHDkKyPZ41HEchL +Af6XQ+pngBQaQ1w9AiFCSsQqZthIbrVlX6MOVtqvvtOWRgQHPtchxI8oIT87dVPh +Ef6Y2sekBHmv7DdedjZ4e3xkwQARSkQqzuciyJKSlxd2e3J/fUyinhssm18/9jI4 +sXew10XECPO52noVVrpPmWDYcDe5n66U09hl9gQsYgHZ+Z7Aubm2tHNaspP+YUp5 +0sCJ+hTYB0W6OHq9VPsH43RjKUmAnBAVNTL4j2BAIjPYPUREDDUlpGkRQnFjdAuF +kP5PxbP3DDNAdsNxCL8h4QfBPCNooSBRxVMz73xjOIHABJfg8bs782ZFhzupumuo +Iz81NWK0rDsmWbGOtbWKsw3h5T/s3c/KQSD9qwLF39TG9hJdKgAh/VrHJEZUAiOZ +y3iFWTw/Ot2IkqJTLQgEgXxHtLTbpAjHQWo3J02LXnLH4xoOYOKvcAApGa5Kh+zT +xvT8hm97tqa2oLd+78oVFoEQ8/dw6dQw61LTdr4VItidTOAFKECq4fLz2Mv2mFO7 +7cqSiyJncixvjxzhlBkbYXqFzVHeiMxrWmV86ga4MSuV/4oVnwc+o0CuBQihMTQ= +-----END CERTIFICATE----- diff --git a/spec/lib/acts_as_journalized/journable_differ_spec.rb b/spec/lib/acts_as_journalized/journable_differ_spec.rb index 8f84fd58738..fb93bb9d6a9 100644 --- a/spec/lib/acts_as_journalized/journable_differ_spec.rb +++ b/spec/lib/acts_as_journalized/journable_differ_spec.rb @@ -74,7 +74,7 @@ RSpec.describe Acts::Journalized::JournableDiffer do status_id: 45, schedule_manually: nil, ignore_non_working_days: false, - estimated_hours: 1) + estimated_hours: nil) end let(:changed) do build_stubbed(:journal_work_package_journal, @@ -86,7 +86,7 @@ RSpec.describe Acts::Journalized::JournableDiffer do status_id: original.status_id + 12, schedule_manually: false, ignore_non_working_days: true, - estimated_hours: nil) + estimated_hours: 1) end it 'returns the changes' do @@ -97,7 +97,38 @@ RSpec.describe Acts::Journalized::JournableDiffer do "status_id" => [original.status_id, changed.status_id], "schedule_manually" => [nil, false], "ignore_non_working_days" => [false, true], - "estimated_hours" => [1.0, nil]) + "estimated_hours" => [nil, 1.0]) + end + end + end + + describe '.association_changes' do + context 'when the objects are work packages' do + let(:original) do + build(:work_package, + custom_values: [ + build_stubbed(:work_package_custom_value, custom_field_id: 1, value: 1), + build_stubbed(:work_package_custom_value, custom_field_id: 2, value: nil), + build_stubbed(:work_package_custom_value, custom_field_id: 3, value: "") + ]) + end + + let(:changed) do + build(:work_package, + custom_values: [ + build_stubbed(:work_package_custom_value, custom_field_id: 1, value: ""), + build_stubbed(:work_package_custom_value, custom_field_id: 2, value: ""), + build_stubbed(:work_package_custom_value, custom_field_id: 3, value: 2) + ]) + end + + it 'returns the changes' do + params = [original, changed, 'custom_values', 'custom_field', :custom_field_id, :value] + expect(described_class.association_changes(*params)) + .to eql( + "custom_field_1" => ["1", ""], + "custom_field_3" => ["", "2"] + ) end end end diff --git a/spec/lib/api/v3/work_packages/eager_loading/cache_checksum_integration_spec.rb b/spec/lib/api/v3/work_packages/eager_loading/cache_checksum_integration_spec.rb index db9d70e12cc..38fee258cb2 100644 --- a/spec/lib/api/v3/work_packages/eager_loading/cache_checksum_integration_spec.rb +++ b/spec/lib/api/v3/work_packages/eager_loading/cache_checksum_integration_spec.rb @@ -27,22 +27,25 @@ #++require 'rspec' require 'spec_helper' -require_relative './eager_loading_mock_wrapper' +require_relative 'eager_loading_mock_wrapper' RSpec.describe API::V3::WorkPackages::EagerLoading::Checksum do + let(:project) { create(:project) } let(:responsible) { create(:user) } let(:assignee) { create(:user) } let(:category) { create(:category) } let(:version) { create(:version) } + let(:budget) { create(:budget, project:) } let!(:work_package) do create(:work_package, + project:, responsible:, assigned_to: assignee, + budget:, version:, category:) end let!(:type) { work_package.type } - let!(:project) { work_package.project } describe '.apply' do let!(:orig_checksum) do @@ -69,7 +72,7 @@ RSpec.describe API::V3::WorkPackages::EagerLoading::Checksum do end it 'produces a different checksum on changes to the status' do - work_package.status.update_attribute(:updated_at, Time.now + 10.seconds) + work_package.status.update_attribute(:updated_at, 10.seconds.from_now) expect(new_checksum) .not_to eql orig_checksum @@ -83,7 +86,7 @@ RSpec.describe API::V3::WorkPackages::EagerLoading::Checksum do end it 'produces a different checksum on changes to the author' do - work_package.author.update_attribute(:updated_at, Time.now + 10.seconds) + work_package.author.update_attribute(:updated_at, 10.seconds.from_now) expect(new_checksum) .not_to eql orig_checksum @@ -97,7 +100,7 @@ RSpec.describe API::V3::WorkPackages::EagerLoading::Checksum do end it 'produces a different checksum on changes to the assigned_to' do - work_package.assigned_to.update_attribute(:updated_at, Time.now + 10.seconds) + work_package.assigned_to.update_attribute(:updated_at, 10.seconds.from_now) expect(new_checksum) .not_to eql orig_checksum @@ -111,7 +114,7 @@ RSpec.describe API::V3::WorkPackages::EagerLoading::Checksum do end it 'produces a different checksum on changes to the responsible' do - work_package.responsible.update_attribute(:updated_at, Time.now + 10.seconds) + work_package.responsible.update_attribute(:updated_at, 10.seconds.from_now) expect(new_checksum) .not_to eql orig_checksum @@ -125,7 +128,7 @@ RSpec.describe API::V3::WorkPackages::EagerLoading::Checksum do end it 'produces a different checksum on changes to the version' do - work_package.version.update_attribute(:updated_at, Time.now + 10.seconds) + work_package.version.update_attribute(:updated_at, 10.seconds.from_now) expect(new_checksum) .not_to eql orig_checksum @@ -140,7 +143,7 @@ RSpec.describe API::V3::WorkPackages::EagerLoading::Checksum do end it 'produces a different checksum on changes to the type' do - work_package.type.update_attribute(:updated_at, Time.now + 10.seconds) + work_package.type.update_attribute(:updated_at, 10.seconds.from_now) expect(new_checksum) .not_to eql orig_checksum @@ -154,7 +157,7 @@ RSpec.describe API::V3::WorkPackages::EagerLoading::Checksum do end it 'produces a different checksum on changes to the priority' do - work_package.priority.update_attribute(:updated_at, Time.now + 10.seconds) + work_package.priority.update_attribute(:updated_at, 10.seconds.from_now) expect(new_checksum) .not_to eql orig_checksum @@ -168,7 +171,21 @@ RSpec.describe API::V3::WorkPackages::EagerLoading::Checksum do end it 'produces a different checksum on changes to the category' do - work_package.category.update_attribute(:updated_at, Time.now + 10.seconds) + work_package.category.update_attribute(:updated_at, 10.seconds.from_now) + + expect(new_checksum) + .not_to eql orig_checksum + end + + it 'produces a different checksum on changes to the budget id' do + WorkPackage.where(id: work_package.id).update_all(budget_id: 0) + + expect(new_checksum) + .not_to eql orig_checksum + end + + it 'produces a different checksum on changes to the budget' do + work_package.budget.update_attribute(:updated_at, 10.seconds.from_now) expect(new_checksum) .not_to eql orig_checksum diff --git a/spec/lib/api/v3/work_packages/eager_loading/custom_value_integration_spec.rb b/spec/lib/api/v3/work_packages/eager_loading/custom_value_integration_spec.rb deleted file mode 100644 index f903e35530a..00000000000 --- a/spec/lib/api/v3/work_packages/eager_loading/custom_value_integration_spec.rb +++ /dev/null @@ -1,191 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2023 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++require 'rspec' - -require 'spec_helper' -require_relative './eager_loading_mock_wrapper' - -RSpec.describe API::V3::WorkPackages::EagerLoading::CustomValue do - let!(:work_package) { create(:work_package) } - let!(:type) { work_package.type } - let!(:other_type) { create(:type) } - let!(:project) { work_package.project } - let!(:other_project) { create(:project) } - let!(:user) { create(:user) } - let!(:version) { create(:version, project:) } - - describe 'multiple CFs' do - let!(:type_project_list_cf) do - create(:list_wp_custom_field).tap do |cf| - type.custom_fields << cf - project.work_package_custom_fields << cf - end - end - let!(:type_project_user_cf) do - create(:user_wp_custom_field).tap do |cf| - type.custom_fields << cf - project.work_package_custom_fields << cf - end - end - let!(:type_project_version_cf) do - create(:version_wp_custom_field, name: 'blubs').tap do |cf| - type.custom_fields << cf - project.work_package_custom_fields << cf - end - end - let!(:for_all_type_cf) do - create(:list_wp_custom_field, is_for_all: true).tap do |cf| - type.custom_fields << cf - end - end - let!(:for_all_other_type_cf) do - create(:list_wp_custom_field, is_for_all: true).tap do |cf| - other_type.custom_fields << cf - end - end - let!(:type_other_project_cf) do - create(:list_wp_custom_field).tap do |cf| - type.custom_fields << cf - other_project.work_package_custom_fields << cf - end - end - let!(:other_type_project_cf) do - create(:list_wp_custom_field).tap do |cf| - other_type.custom_fields << cf - project.work_package_custom_fields << cf - end - end - - describe '.apply' do - it 'preloads the custom fields and values' do - create(:custom_value, - custom_field: type_project_list_cf, - customized: work_package, - value: type_project_list_cf.custom_options.last.id) - - build(:custom_value, - custom_field: type_project_user_cf, - customized: work_package, - value: user.id) - .save(validate: false) - - create(:custom_value, - custom_field: type_project_version_cf, - customized: work_package, - value: version.id) - - work_package = WorkPackage.first - wrapped = EagerLoadingMockWrapper.wrap(described_class, [work_package]) - - expect(type) - .not_to receive(:custom_fields) - expect(project) - .not_to receive(:all_work_package_custom_fields) - - [CustomOption, User, Version].each do |klass| - expect(klass) - .not_to receive(:find_by) - end - - wrapped.each do |w| - expect(w.available_custom_fields) - .to match_array [type_project_list_cf, - type_project_version_cf, - type_project_user_cf, - for_all_type_cf] - - expect(work_package.send(type_project_version_cf.attribute_getter)) - .to eql version - expect(work_package.send(type_project_list_cf.attribute_getter)) - .to eql type_project_list_cf.custom_options.last.name - expect(work_package.send(type_project_user_cf.attribute_getter)) - .to eql user - end - end - end - end - - describe '#usages returning an is_for_all custom field within one project (Regression #28435)' do - let!(:for_all_type_cf) do - create(:list_wp_custom_field, is_for_all: true).tap do |cf| - type.custom_fields << cf - end - end - let(:other_project) { create(:project) } - - subject { described_class.new [work_package] } - - before do - # Assume that one custom field has an entry in project_custom_fields - for_all_type_cf.projects << other_project - end - - it 'still allows looking up the global custom field in a different project' do - # Exhibits the same behavior as in regression, usage returns a hash with project_id set for a global - # custom field - expect(for_all_type_cf.is_for_all).to be(true) - expect(subject.send(:usages)) - .to include("project_id" => other_project.id, "type_id" => type.id, "custom_field_id" => for_all_type_cf.id) - - wrapped = EagerLoadingMockWrapper.wrap(described_class, [work_package]) - expect(wrapped.first.available_custom_fields).to include(for_all_type_cf) - end - end - - describe '#usages returning an is_for_all custom field within multiple projects (Regression #28452)' do - let!(:for_all_type_cf) do - create(:list_wp_custom_field, is_for_all: true).tap do |cf| - type.custom_fields << cf - end - end - let(:other_project) { create(:project) } - let(:other_project2) { create(:project) } - - subject { described_class.new [work_package] } - - before do - # Assume that one custom field has an entry in project_custom_fields - for_all_type_cf.projects << other_project - for_all_type_cf.projects << other_project2 - end - - it 'does not double add the custom field to the available CFs' do - # Exhibits the same behavior as in regression, usage returns a hash with project_id set for a global - # custom field - expect(for_all_type_cf.is_for_all).to be(true) - expect(subject.send(:usages)) - .to include("project_id" => other_project.id, "type_id" => type.id, "custom_field_id" => for_all_type_cf.id) - - expect(subject.send(:usages)) - .to include("project_id" => other_project2.id, "type_id" => type.id, "custom_field_id" => for_all_type_cf.id) - - wrapped = EagerLoadingMockWrapper.wrap(described_class, [work_package]) - expect(wrapped.first.available_custom_fields.length).to eq(1) - expect(wrapped.first.available_custom_fields.to_a).to eq([for_all_type_cf]) - end - end -end diff --git a/spec/lib/journal_formatter/attachment_spec.rb b/spec/lib/journal_formatter/attachment_spec.rb index 1af4d6f2675..d7017aff8cc 100644 --- a/spec/lib/journal_formatter/attachment_spec.rb +++ b/spec/lib/journal_formatter/attachment_spec.rb @@ -40,25 +40,19 @@ RSpec.describe OpenProject::JournalFormatter::Attachment do { only_path: true } end - let(:klass) { OpenProject::JournalFormatter::Attachment } - let(:instance) { klass.new(journal) } - let(:id) { 1 } - let(:journal) do - OpenStruct.new(id:) - end + let(:journal) { instance_double(Journal, id: 1) } let(:user) { create(:user) } - let(:attachment) do - create(:attachment, - author: user) - end + let(:attachment) { create(:attachment, author: user) } let(:key) { "attachments_#{attachment.id}" } + subject(:instance) { described_class.new(journal) } + describe '#render' do describe 'WITH the first value being nil, and the second an id as string' do it 'adds an attachment added text' do link = "#{Setting.protocol}://#{Setting.host_name}/api/v3/attachments/#{attachment.id}/content" - expect(instance.render(key, [nil, attachment.id.to_s])) - .to eq(I18n.t(:text_journal_added, + expect(instance.render(key, [nil, attachment.filename.to_s])) + .to eq(I18n.t(:text_journal_attachment_added, label: "#{I18n.t(:'activerecord.models.attachment')}", value: "#{attachment.filename}")) end @@ -72,8 +66,8 @@ RSpec.describe OpenProject::JournalFormatter::Attachment do it 'adds an attachment added text' do link = "#{Setting.protocol}://#{Setting.host_name}/blubs/api/v3/attachments/#{attachment.id}/content" - expect(instance.render(key, [nil, attachment.id.to_s])) - .to eq(I18n.t(:text_journal_added, + expect(instance.render(key, [nil, attachment.filename.to_s])) + .to eq(I18n.t(:text_journal_attachment_added, label: "#{I18n.t(:'activerecord.models.attachment')}", value: "#{attachment.filename}")) end @@ -82,34 +76,34 @@ RSpec.describe OpenProject::JournalFormatter::Attachment do describe 'WITH the first value being an id as string, and the second nil' do let(:expected) do - I18n.t(:text_journal_deleted, + I18n.t(:text_journal_attachment_deleted, label: "#{I18n.t(:'activerecord.models.attachment')}", - old: "#{attachment.id}") + old: "#{attachment.filename}") end - it { expect(instance.render(key, [attachment.id.to_s, nil])).to eq(expected) } + it { expect(instance.render(key, [attachment.filename.to_s, nil])).to eq(expected) } end describe "WITH the first value being nil, and the second an id as a string WITH specifying not to output html" do let(:expected) do - I18n.t(:text_journal_added, + I18n.t(:text_journal_attachment_added, label: I18n.t(:'activerecord.models.attachment'), - value: attachment.id) + value: attachment.filename) end - it { expect(instance.render(key, [nil, attachment.id.to_s], html: false)).to eq(expected) } + it { expect(instance.render(key, [nil, attachment.filename.to_s], html: false)).to eq(expected) } end describe "WITH the first value being an id as string, and the second nil, WITH specifying not to output html" do let(:expected) do - I18n.t(:text_journal_deleted, + I18n.t(:text_journal_attachment_deleted, label: I18n.t(:'activerecord.models.attachment'), - old: attachment.id) + old: attachment.filename) end - it { expect(instance.render(key, [attachment.id.to_s, nil], html: false)).to eq(expected) } + it { expect(instance.render(key, [attachment.filename.to_s, nil], html: false)).to eq(expected) } end end end diff --git a/spec/lib/journal_formatter/cause_spec.rb b/spec/lib/journal_formatter/cause_spec.rb index f88620f0430..1840a76d194 100644 --- a/spec/lib/journal_formatter/cause_spec.rb +++ b/spec/lib/journal_formatter/cause_spec.rb @@ -282,4 +282,31 @@ RSpec.describe OpenProject::JournalFormatter::Cause do end end end + + context 'when the change was caused by a system update' do + let(:cause) do + { + "type" => 'system_update', + "feature" => 'file_links_journal' + } + end + + context 'when rendering HTML variant' do + let(:html) { true } + + it do + expect(subject).to eq "#{I18n.t('journals.caused_changes.system_update')} " \ + "#{I18n.t('journals.cause_descriptions.system_update.file_links_journal')}" + end + end + + context 'when rendering raw variant' do + let(:html) { false } + + it do + expect(subject).to eq "#{I18n.t('journals.caused_changes.system_update')} " \ + "#{I18n.t('journals.cause_descriptions.system_update.file_links_journal')}" + end + end + end end diff --git a/spec/lib/journal_formatter/file_link_spec.rb b/spec/lib/journal_formatter/file_link_spec.rb index ed09fa58b40..9208cec7d9e 100644 --- a/spec/lib/journal_formatter/file_link_spec.rb +++ b/spec/lib/journal_formatter/file_link_spec.rb @@ -31,22 +31,47 @@ require 'spec_helper' RSpec.describe OpenProject::JournalFormatter::FileLink do - let(:work_package) { create(:work_package) } + let(:work_package) { build(:work_package) } let(:journal) { instance_double(Journal, journable: work_package) } - let(:file_link) { create(:file_link, container: work_package) } + let(:file_link) { create(:file_link, container: work_package, storage: build(:storage)) } let(:key) { "file_links_#{file_link.id}" } + let(:changes) { { "link_name" => file_link.origin_name, "storage_name" => file_link.storage.name } } + subject(:instance) { described_class.new(journal) } describe '#render' do - context 'having the first value being nil, and the second an file link name as string' do + context 'having the origin_name as nil' do + let(:changes) { { "link_name" => file_link.origin_name, "storage_name" => nil } } + + it 'if the storage name is nil it tries to find it out looking at the file link' do + link = "#{Setting.protocol}://#{Setting.host_name}/api/v3/file_links/#{file_link.id}/open" + + expect(instance.render(key, [nil, changes])) + .to eq(I18n.t(:text_journal_file_link_added, + label: "#{I18n.t('activerecord.models.file_link')}", + value: "#{file_link.origin_name}", + storage: file_link.storage.name)) + end + + it 'if the storage name is nil and the file link does not exist, "Unknown storage" is used' do + expect(instance.render('file_links_12', [changes, nil])) + .to eq(I18n.t(:text_journal_file_link_deleted, + label: "#{I18n.t('activerecord.models.file_link')}", + old: "#{file_link.origin_name}", + storage: I18n.t('storages.unknown_storage'))) + end + end + + context 'having the first value being nil, and the second an hash of properties' do context 'as HTML' do it 'adds a file link added text' do link = "#{Setting.protocol}://#{Setting.host_name}/api/v3/file_links/#{file_link.id}/open" - expect(instance.render(key, [nil, file_link.origin_name])) - .to eq(I18n.t(:text_journal_added, - label: "#{I18n.t(:'activerecord.models.file_link')}", - value: "#{file_link.origin_name}")) + expect(instance.render(key, [nil, changes])) + .to eq(I18n.t(:text_journal_file_link_added, + label: "#{I18n.t('activerecord.models.file_link')}", + value: "#{file_link.origin_name}", + storage: file_link.storage.name)) end context 'with a configured relative url root' do @@ -54,41 +79,47 @@ RSpec.describe OpenProject::JournalFormatter::FileLink do it 'adds an file link added text' do link = "#{Setting.protocol}://#{Setting.host_name}/blubs/api/v3/file_links/#{file_link.id}/open" - expect(instance.render(key, [nil, file_link.origin_name])) - .to eq(I18n.t(:text_journal_added, - label: "#{I18n.t(:'activerecord.models.file_link')}", - value: "#{file_link.origin_name}")) + expect(instance.render(key, [nil, changes])) + .to eq(I18n.t(:text_journal_file_link_added, + label: "#{I18n.t('activerecord.models.file_link')}", + value: "#{file_link.origin_name}", + storage: file_link.storage.name)) end end end context 'as plain text' do it 'adds a file link added text' do - message = I18n.t(:text_journal_added, - label: I18n.t(:'activerecord.models.file_link'), - value: file_link.id) + message = I18n.t(:text_journal_file_link_added, + label: I18n.t('activerecord.models.file_link'), + value: file_link.origin_name, + storage: file_link.storage.name) - expect(instance.render(key, [nil, file_link.id.to_s], html: false)).to eq(message) + expect(instance.render(key, [nil, changes], html: false)).to eq(message) end end end - context 'having the first value being an id as string, and the second nil' do + context 'having the first value being an props hash, and the second nil' do context 'as HTML' do it 'adds a file link remove text' do - message = I18n.t(:text_journal_deleted, - label: "#{I18n.t(:'activerecord.models.file_link')}", - old: "#{file_link.id}") + message = I18n.t(:text_journal_file_link_deleted, + label: "#{I18n.t('activerecord.models.file_link')}", + old: "#{file_link.origin_name}", + storage: file_link.storage.name) - expect(instance.render(key, [file_link.id.to_s, nil])).to eq(message) + expect(instance.render(key, [changes, nil])).to eq(message) end end context 'as plain text' do it 'adds a file link removed' do - message = I18n.t(:text_journal_deleted, label: Storages::FileLink.model_name.human, old: file_link.id) + message = I18n.t(:text_journal_file_link_deleted, + label: I18n.t('activerecord.models.file_link'), + old: file_link.origin_name, + storage: file_link.storage.name) - expect(instance.render(key, [file_link.id.to_s, nil], html: false)).to eq(message) + expect(instance.render(key, [changes, nil], html: false)).to eq(message) end end end diff --git a/spec/lib/open_project/events_spec.rb b/spec/lib/open_project/events_spec.rb new file mode 100644 index 00000000000..c9f6c255486 --- /dev/null +++ b/spec/lib/open_project/events_spec.rb @@ -0,0 +1,167 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' + +RSpec.describe OpenProject::Events do + def fire_event(event_constant_name) + OpenProject::Notifications.send( + "#{described_class}::#{event_constant_name}".constantize, + payload + ) + end + + %w[ + PROJECT_STORAGE_CREATED + PROJECT_STORAGE_UPDATED + PROJECT_STORAGE_DESTROYED + ].each do |event| + describe(event) do + subject { fire_event(event) } + + context 'when payload is empty' do + let(:payload) { {} } + + it do + expect { subject }.not_to change(enqueued_jobs, :count) + end + end + + context 'when payload contains automatic project_folder_mpde' do + let(:payload) { { project_folder_mode: :automatic } } + + it do + expect { subject }.to change(enqueued_jobs, :count).from(0).to(1) + end + + it do + subject + expect(enqueued_jobs[0][:job]).to eq(Storages::ManageNextcloudIntegrationEventsJob) + end + end + end + end + + %w[ + MEMBER_CREATED + MEMBER_UPDATED + MEMBER_DESTROYED + PROJECT_UPDATED + PROJECT_RENAMED + ].each do |event| + describe(event) do + subject { fire_event(event) } + + let(:payload) { {} } + + it do + expect { subject }.to change(enqueued_jobs, :count).from(0).to(1) + end + + it do + subject + expect(enqueued_jobs[0][:job]).to eq(Storages::ManageNextcloudIntegrationEventsJob) + end + end + end + + describe 'OAUTH_CLIENT_TOKEN_CREATED' do + subject { fire_event('OAUTH_CLIENT_TOKEN_CREATED') } + + context 'when payload is empty' do + let(:payload) { {} } + + it do + expect { subject }.not_to change(enqueued_jobs, :count) + end + end + + context 'when payload contains storage integration type' do + let(:payload) { { integration_type: 'Storages::Storage' } } + + it do + expect { subject }.to change(enqueued_jobs, :count).from(0).to(1) + end + + it do + subject + expect(enqueued_jobs[0][:job]).to eq(Storages::ManageNextcloudIntegrationEventsJob) + end + end + end + + describe 'ROLE_UPDATED' do + subject { fire_event('ROLE_UPDATED') } + + context 'when payload is empty' do + let(:payload) { {} } + + it do + expect { subject }.not_to change(enqueued_jobs, :count) + end + end + + context 'when payload contains some nextcloud related permissions as a diff' do + let(:payload) { { permissions_diff: [:read_files] } } + + it do + expect { subject }.to change(enqueued_jobs, :count).from(0).to(1) + end + + it do + subject + expect(enqueued_jobs[0][:job]).to eq(Storages::ManageNextcloudIntegrationEventsJob) + end + end + end + + describe 'ROLE_DESTROYED' do + subject { fire_event('ROLE_DESTROYED') } + + context 'when payload is empty' do + let(:payload) { {} } + + it do + expect { subject }.not_to change(enqueued_jobs, :count) + end + end + + context 'when payload contains some nextcloud related permissions' do + let(:payload) { { permissions: [:read_files] } } + + it do + expect { subject }.to change(enqueued_jobs, :count).from(0).to(1) + end + + it do + subject + expect(enqueued_jobs[0][:job]).to eq(Storages::ManageNextcloudIntegrationEventsJob) + end + end + end +end diff --git a/spec/lib/open_project/text_formatting/markdown/pandoc_wrapper_spec.rb b/spec/lib/open_project/text_formatting/markdown/pandoc_wrapper_spec.rb deleted file mode 100644 index d5d40ccffb5..00000000000 --- a/spec/lib/open_project/text_formatting/markdown/pandoc_wrapper_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2023 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require 'spec_helper' - -RSpec.describe OpenProject::TextFormatting::Formats::Markdown::PandocWrapper do - let(:subject) { described_class.new } - - before do - allow(subject).to receive(:read_usage_string).and_return(usage_string) - end - - describe 'wrap mode' do - context 'when wrap=preserve exists' do - let(:usage_string) do - <<~EOS - --list-output-formats#{' '} - --list-highlight-languages#{' '} - --list-highlight-styles#{' '} - --wrap=auto|none|preserve - -v --version#{' '} - -h --help - EOS - end - - it do - expect(subject.wrap_mode).to eq('--wrap=preserve') - end - end - - context 'when only no-wrap exists' do - let(:usage_string) do - <<~EOS - --list-output-formats#{' '} - --list-highlight-languages#{' '} - --list-highlight-styles#{' '} - --no-wrap - -v --version#{' '} - -h --help - EOS - end - - it do - expect(subject.wrap_mode).to eq('--no-wrap') - end - end - - context 'when neither exists' do - let(:usage_string) { 'wat?' } - - it do - expect do - subject.wrap_mode - end.to raise_error 'Your pandoc version has neither --no-wrap nor --wrap=preserve. Please install a recent version of pandoc.' - end - end - end -end diff --git a/spec/lib/redmine/menu_manager/menu_helper_spec.rb b/spec/lib/redmine/menu_manager/menu_helper_spec.rb index 95f75f5e8e6..0ba51e01ac7 100644 --- a/spec/lib/redmine/menu_manager/menu_helper_spec.rb +++ b/spec/lib/redmine/menu_manager/menu_helper_spec.rb @@ -111,12 +111,16 @@ RSpec.describe Redmine::MenuManager::MenuHelper, type: :helper do @@ -202,13 +206,17 @@ RSpec.describe Redmine::MenuManager::MenuHelper, type: :helper do