diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index e2054525de57..74ffad708a47 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -11,3 +11,4 @@ f3c99ee5dded81ad55f2b6f3706216d5fa765677 9e4934cd0a468f46d8f0fc0f11ebc2d4216f789c 6678cab48d443b5782fa93b171d62093819ee4fc fa5d03eae00bc8931f99598a74ffd76e0cbca3da +b10e6d718cc49e3574837d97fab268e3ecb3fcbd diff --git a/.github/workflows/rubocop-core.yml b/.github/workflows/rubocop-core.yml index 59b1ecb0a4c8..d1953e531b68 100644 --- a/.github/workflows/rubocop-core.yml +++ b/.github/workflows/rubocop-core.yml @@ -8,14 +8,20 @@ jobs: name: rubocop runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 2 # we are comparing PR merge head with base + - uses: actions/checkout@v4 + - name: Fetch head commit of base branch + run: git fetch --depth 1 origin ${{ github.event.pull_request.base.sha }} - uses: ruby/setup-ruby@v1 - uses: opf/action-rubocop@master with: github_token: ${{ secrets.github_token }} rubocop_version: gemfile - rubocop_extensions: rubocop-inflector:gemfile rubocop-performance:gemfile rubocop-rails:gemfile rubocop-rspec:gemfile + rubocop_extensions: > + rubocop-capybara:gemfile + rubocop-factory_bot:gemfile + rubocop-performance:gemfile + rubocop-rails:gemfile + rubocop-rspec:gemfile + rubocop-rspec_rails:gemfile reporter: github-pr-check only_changed: true diff --git a/.github/workflows/test-core.yml b/.github/workflows/test-core.yml index 1a458b72b039..e5b9afcf86e0 100644 --- a/.github/workflows/test-core.yml +++ b/.github/workflows/test-core.yml @@ -23,7 +23,11 @@ jobs: all: name: Units + Features if: github.repository == 'opf/openproject' - runs-on: runs-on,runner=32cpu-linux-x64,run-id=${{ github.run_id }} + runs-on: + labels: + - runs-on + - runner=32cpu-linux-x64 + - run-id=${{ github.run_id }} timeout-minutes: 40 env: DOCKER_BUILDKIT: 1 diff --git a/.rubocop.yml b/.rubocop.yml index ef318056fdef..13ea37fa1ef6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -199,6 +199,10 @@ RSpec/DescribeClass: - 'spec/features/**/*.rb' - 'modules/*/spec/features/**/*.rb' +# Allow number HTTP status codes in specs +RSpecRails/HttpStatus: + Enabled: false + # dynamic finders cop clashes with capybara ID cop Rails/DynamicFindBy: Enabled: true @@ -236,10 +240,6 @@ RSpec/DescribeMethod: RSpec/SpecFilePathFormat: IgnoreMethods: true -# Disable deprecated cop -RSpec/FilePath: - Enabled: false - # Prevent "fit" or similar to be committed RSpec/Focus: Enabled: true diff --git a/Gemfile b/Gemfile index e8a9595ae48e..127491dfb297 100644 --- a/Gemfile +++ b/Gemfile @@ -158,7 +158,7 @@ gem "structured_warnings", "~> 0.4.0" gem "airbrake", "~> 13.0.0", require: false gem "markly", "~> 0.10" # another markdown parser like commonmarker, but with AST support used in PDF export -gem "md_to_pdf", git: "https://github.com/opf/md-to-pdf", ref: "8772c791a21819751c0d111be903b3b44ef7d862" +gem "md_to_pdf", git: "https://github.com/opf/md-to-pdf", ref: "32603f09a249999a00e8ca23eb17215b46a26c0f" gem "prawn", "~> 2.4" gem "ttfunk", "~> 1.7.0" # remove after https://github.com/prawnpdf/prawn/issues/1346 resolved. @@ -210,6 +210,7 @@ gem "validate_url" # Storages support code gem "dry-container" +gem "dry-monads" # ActiveRecord extension which adds typecasting to store accessors gem "store_attribute", "~> 1.0" @@ -349,7 +350,7 @@ end gem "bootsnap", "~> 1.18.0", require: false # API gems -gem "grape", "~> 2.0.0" +gem "grape", "~> 2.1.0" gem "grape_logging", "~> 1.8.4" gem "roar", "~> 1.2.0" @@ -390,4 +391,4 @@ end gem "openproject-octicons", "~>19.14.1" gem "openproject-octicons_helper", "~>19.14.1" -gem "openproject-primer_view_components", "~>0.34.0" +gem "openproject-primer_view_components", "~>0.35.2" diff --git a/Gemfile.lock b/Gemfile.lock index fdd274f49745..a817cb875e63 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,16 +8,16 @@ GIT GIT remote: https://github.com/opf/md-to-pdf - revision: 8772c791a21819751c0d111be903b3b44ef7d862 - ref: 8772c791a21819751c0d111be903b3b44ef7d862 + revision: 32603f09a249999a00e8ca23eb17215b46a26c0f + ref: 32603f09a249999a00e8ca23eb17215b46a26c0f specs: - md_to_pdf (0.0.27) + md_to_pdf (0.1.1) color_conversion (~> 0.1) front_matter_parser (~> 1.0) json-schema (~> 4.3) markly (~> 0.10) matrix (~> 0.4) - nokogiri (~> 1.1) + nokogiri (~> 1.16) prawn (~> 2.4) prawn-table (~> 0.2) text-hyphen (~> 1.5) @@ -323,8 +323,8 @@ GEM activesupport (>= 6.1) acts_as_tree (2.9.1) activerecord (>= 3.0.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) aes_key_wrap (1.1.0) afm (0.2.2) airbrake (13.0.4) @@ -342,17 +342,17 @@ GEM activerecord (>= 4.0.0, < 7.2) awrence (1.2.1) aws-eventstream (1.3.0) - aws-partitions (1.945.0) - aws-sdk-core (3.197.1) + aws-partitions (1.947.0) + aws-sdk-core (3.199.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.85.0) - aws-sdk-core (~> 3, >= 3.197.0) + aws-sdk-kms (1.87.0) + aws-sdk-core (~> 3, >= 3.199.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.152.3) - aws-sdk-core (~> 3, >= 3.197.0) + aws-sdk-s3 (1.154.0) + aws-sdk-core (~> 3, >= 3.199.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) aws-sdk-sns (1.77.0) @@ -438,9 +438,9 @@ GEM css_parser (1.17.1) addressable csv (3.3.0) - cuprite (0.15) + cuprite (0.15.1) capybara (~> 3.0) - ferrum (~> 0.14.0) + ferrum (~> 0.15.0) daemons (1.4.1) dalli (3.2.8) date (3.3.4) @@ -459,7 +459,7 @@ GEM disposable (0.6.3) declarative (>= 0.0.9, < 1.0.0) representable (>= 3.1.1, < 4) - doorkeeper (5.7.0) + doorkeeper (5.7.1) railties (>= 5) dotenv (3.1.2) dotenv-rails (3.1.2) @@ -476,6 +476,10 @@ GEM concurrent-ruby (~> 1.0) dry-core (~> 1.0, < 2) zeitwerk (~> 2.6) + dry-monads (1.6.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) dry-types (1.7.2) bigdecimal (~> 3.0) concurrent-ruby (~> 1.0) @@ -525,11 +529,11 @@ GEM faraday-net_http (3.1.0) net-http fastimage (2.3.1) - ferrum (0.14) + ferrum (0.15) addressable (~> 2.5) concurrent-ruby (~> 1.1) webrick (~> 1.7) - websocket-driver (>= 0.6, < 0.8) + websocket-driver (~> 0.7) ffi (1.17.0) flamegraph (0.9.5) fog-aws (3.23.0) @@ -591,13 +595,12 @@ GEM multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) - grape (2.0.0) - activesupport (>= 5) - builder + grape (2.1.0) + activesupport (>= 6) dry-types (>= 1.1) - mustermann-grape (~> 1.0.0) - rack (>= 1.3.0) - rack-accept + mustermann-grape (~> 1.1.0) + rack (>= 2) + zeitwerk grape_logging (1.8.4) grape rack @@ -670,7 +673,7 @@ GEM launchy (3.0.1) addressable (~> 2.8) childprocess (~> 5.0) - lefthook (1.6.17) + lefthook (1.6.18) letter_opener (1.10.0) launchy (>= 2.2, < 4) letter_opener_web (3.0.0) @@ -694,7 +697,7 @@ GEM loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - lookbook (2.3.1) + lookbook (2.3.2) activemodel css_parser htmlbeautifier (~> 1.3) @@ -729,7 +732,7 @@ GEM multi_json (1.15.0) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) - mustermann-grape (1.0.2) + mustermann-grape (1.1.0) mustermann (>= 1.0.0) mutex_m (0.2.0) net-http (0.4.1) @@ -773,7 +776,7 @@ GEM actionview openproject-octicons (= 19.14.1) railties - openproject-primer_view_components (0.34.0) + openproject-primer_view_components (0.35.2) actionview (>= 5.0.0) activesupport (>= 5.0.0) openproject-octicons (>= 19.12.0) @@ -829,7 +832,7 @@ GEM pry (>= 0.12.0) psych (5.1.2) stringio - public_suffix (5.0.5) + public_suffix (6.0.0) puffing-billy (4.0.0) addressable (~> 2.5) em-http-request (~> 1.1, >= 1.1.0) @@ -845,8 +848,6 @@ GEM raabro (1.4.0) racc (1.8.0) rack (2.2.9) - rack-accept (0.4.5) - rack (>= 0.4) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-cors (2.0.2) @@ -1195,6 +1196,7 @@ DEPENDENCIES doorkeeper (~> 5.7.0) dotenv-rails dry-container + dry-monads email_validator (~> 2.2.3) equivalent-xml (~> 0.6) erb_lint @@ -1211,7 +1213,7 @@ DEPENDENCIES good_job (= 3.26.2) google-apis-gmail_v1 googleauth - grape (~> 2.0.0) + grape (~> 2.1.0) grape_logging (~> 1.8.4) grids! html-pipeline (~> 2.14.0) @@ -1262,7 +1264,7 @@ DEPENDENCIES openproject-octicons (~> 19.14.1) openproject-octicons_helper (~> 19.14.1) openproject-openid_connect! - openproject-primer_view_components (~> 0.34.0) + openproject-primer_view_components (~> 0.35.2) openproject-recaptcha! openproject-reporting! openproject-storages! diff --git a/app/components/_index.sass b/app/components/_index.sass index fa7b42a43c7f..a05d3e6b1e6e 100644 --- a/app/components/_index.sass +++ b/app/components/_index.sass @@ -1,5 +1,5 @@ -@import "work_packages/share/modal_body_component" -@import "work_packages/share/invite_user_form_component" +@import "shares/modal_body_component" +@import "shares/invite_user_form_component" @import "work_packages/progress/modal_body_component" @import "open_project/common/attribute_component" @import "filter/filters_component" diff --git a/app/components/admin/attachments_settings_header_component.rb b/app/components/admin/attachments_settings_header_component.rb index f9a955648a2e..88d13b0c1808 100644 --- a/app/components/admin/attachments_settings_header_component.rb +++ b/app/components/admin/attachments_settings_header_component.rb @@ -31,7 +31,8 @@ module Admin class AttachmentsSettingsHeaderComponent < ApplicationComponent def initialize(title:, selected:) - raise 'selected must 1, 2 or 3' if [1, 2, 3].exclude?(selected) + raise "selected must 1, 2 or 3" if [1, 2, 3].exclude?(selected) + @title = title @selected = selected end diff --git a/app/components/members/index_page_header_component.rb b/app/components/members/index_page_header_component.rb index 907a6cc2f4e3..70c416a30191 100644 --- a/app/components/members/index_page_header_component.rb +++ b/app/components/members/index_page_header_component.rb @@ -31,7 +31,6 @@ class Members::IndexPageHeaderComponent < ApplicationComponent include OpPrimer::ComponentHelpers include ApplicationHelper - include Menus::MembersHelper def initialize(project: nil) super @@ -77,7 +76,7 @@ def current_query query_name = nil menu_header = nil - first_level_menu_items.find do |section| + Members::Menu.new(project: @project, params:).menu_items.find do |section| section.children.find do |menu_query| if !!menu_query.selected query_name = menu_query.title diff --git a/app/components/members/user_filter_component.rb b/app/components/members/user_filter_component.rb index 43180102d97a..0ef24a8de7fa 100644 --- a/app/components/members/user_filter_component.rb +++ b/app/components/members/user_filter_component.rb @@ -103,11 +103,11 @@ def builtin_share_roles def mapped_shared_role_name(role) case role.builtin when Role::BUILTIN_WORK_PACKAGE_VIEWER - I18n.t("work_package.sharing.permissions.view") + I18n.t("work_package.permissions.view") when Role::BUILTIN_WORK_PACKAGE_COMMENTER - I18n.t("work_package.sharing.permissions.comment") + I18n.t("work_package.permissions.comment") when Role::BUILTIN_WORK_PACKAGE_EDITOR - I18n.t("work_package.sharing.permissions.edit") + I18n.t("work_package.permissions.edit") else role.name end diff --git a/app/components/open_project/common/submenu_component.html.erb b/app/components/open_project/common/submenu_component.html.erb index 35e4f8e2a954..5b38527ce187 100644 --- a/app/components/open_project/common/submenu_component.html.erb +++ b/app/components/open_project/common/submenu_component.html.erb @@ -31,7 +31,7 @@ <% top_level_sidebar_menu_items.first.children.each do |menu_item| %>
  • <% selected = menu_item.selected ? 'selected' : '' %> - + <%= menu_item.title %>
  • @@ -60,7 +60,7 @@ <% menu_item.children.each do |child_item| %>
  • <% selected = child_item.selected ? 'selected' : '' %> - + <%= child_item.title %>
  • @@ -75,9 +75,14 @@ <%= render Primer::Beta::Button.new(scheme: :primary, tag: :a, href: @create_btn_options[:href], + test_selector: "#{@create_btn_options[:module_key]}--create-button", classes: "op-sidebar--footer-action") do |button| button.with_leading_visual_icon(icon: "plus") - @create_btn_options[:text] + if @create_btn_options[:btn_text].present? + @create_btn_options[:btn_text] + else + I18n.t("label_#{@create_btn_options[:module_key]}") + end end %> <% end %> diff --git a/app/components/projects/delete_list_modal_component.html.erb b/app/components/projects/delete_list_modal_component.html.erb index 7fdac5dc3e9d..c57d5e8ee1d0 100644 --- a/app/components/projects/delete_list_modal_component.html.erb +++ b/app/components/projects/delete_list_modal_component.html.erb @@ -4,13 +4,12 @@ data: { 'test-selector': MODAL_ID })) do |d| %> <% d.with_header(variant: :large, mb: 2) %> <% d.with_body { t(:'projects.lists.delete_modal.text') } %> - <% d.with_footer do %> <%= render(Primer::Beta::Button.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) } %> - <%= form_with(url: projects_query_path(query), + <%= form_with(url: project_query_path(query), method: :delete) do %> <%= render(Primer::Beta::Button.new(scheme: :danger, type: :submit)) { I18n.t(:button_delete) } %> - <% end %> + <% end %> <% end %> <% end %> diff --git a/app/components/projects/index_page_header_component.html.erb b/app/components/projects/index_page_header_component.html.erb index 39d48fa1676b..6e02e530b605 100644 --- a/app/components/projects/index_page_header_component.html.erb +++ b/app/components/projects/index_page_header_component.html.erb @@ -9,7 +9,7 @@ header:, message: t("lists.can_be_saved"), label: t("button_save"), - href: projects_query_path(query, projects_query_params), + href: project_query_path(query, projects_query_params), method: :patch ) elsif can_save_as? @@ -17,7 +17,7 @@ header:, message: t("lists.can_be_saved_as"), label: t("button_save_as"), - href: new_projects_query_path(projects_query_params) + href: new_project_query_path(projects_query_params) ) end @@ -32,7 +32,7 @@ if can_rename? menu.with_item( label: t('button_rename'), - href: rename_projects_query_path(query), + href: rename_project_query_path(query), ) do |item| item.with_leading_visual_icon(icon: :pencil) end @@ -62,7 +62,7 @@ menu_save_item( menu:, label: t('button_save'), - href: projects_query_path(query, projects_query_params), + href: project_query_path(query, projects_query_params), method: :patch ) end @@ -71,7 +71,7 @@ menu_save_item( menu:, label: t('button_save_as'), - href: new_projects_query_path(projects_query_params) + href: new_project_query_path(projects_query_params) ) end @@ -96,7 +96,7 @@ menu.with_item( label: t(:button_unpublish), scheme: :danger, - href: unpublish_projects_query_path(query), + href: unpublish_project_query_path(query), content_arguments: { data: { method: :post } } ) do |item| item.with_leading_visual_icon(icon: 'eye-closed') @@ -105,7 +105,7 @@ menu.with_item( label: t(:button_publish), scheme: :default, - href: publish_projects_query_path(query), + href: publish_project_query_path(query), content_arguments: { data: { method: :post } } ) do |item| item.with_leading_visual_icon(icon: 'eye') @@ -132,7 +132,7 @@ render(Primer::OpenProject::PageHeader.new) do |header| header.with_title(data: { 'test-selector': 'project-query-name'}) do primer_form_with(model: query, - url: @query.new_record? ? projects_queries_path(projects_query_params) : projects_query_path(@query, projects_query_params), + url: @query.new_record? ? project_queries_path(projects_query_params) : project_query_path(@query, projects_query_params), scope: 'query', id: 'project-save-form') do |f| render( diff --git a/app/components/projects/index_page_header_component.rb b/app/components/projects/index_page_header_component.rb index f75f4e4b7b5b..6d4d5b121199 100644 --- a/app/components/projects/index_page_header_component.rb +++ b/app/components/projects/index_page_header_component.rb @@ -126,9 +126,9 @@ def current_breadcrumb_element def current_section return @current_section if defined?(@current_section) - projects_menu = Menus::Projects.new(controller_path:, params:, current_user:) + projects_menu = Projects::Menu.new(controller_path:, params:, current_user:) - @current_section = projects_menu.first_level_menu_items.find { |section| section.children.any?(&:selected) } + @current_section = projects_menu.menu_items.find { |section| section.children.any?(&:selected) } end def header_save_action(header:, message:, label:, href:, method: nil) diff --git a/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_component.html.erb b/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_component.html.erb index a7c014f7b739..6fe98a3cdc67 100644 --- a/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_component.html.erb +++ b/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_component.html.erb @@ -1,44 +1,18 @@ <%= - component_wrapper do - render( - Primer::Alpha::Dialog.new( - id: dialog_id, - title:, - test_selector: dialog_id, - size: :large - ) - ) do |dialog| - dialog.with_show_button(scheme: :primary) do |button| - button.with_leading_visual_icon(icon: 'op-include-projects') - show_button_text - end + render( + Primer::Alpha::Dialog.new( + id: Settings::ProjectCustomFields::ProjectCustomFieldMapping::NewProjectMappingFormComponent::DIALOG_ID, + title:, + test_selector: Settings::ProjectCustomFields::ProjectCustomFieldMapping::NewProjectMappingFormComponent::DIALOG_ID, + size: :large + ) + ) do |dialog| + dialog.with_header( + show_divider: false, + visually_hide_title: false, + variant: :large + ) - dialog.with_header( - show_divider: false, - visually_hide_title: false, - variant: :large - ) - - primer_form_with( - class: "op-new-project-mapping-form", - model: @project_mapping, - url: link_admin_settings_project_custom_field_path(@project_custom_field), - data: { turbo: true }, - method: :post - ) do |form| - concat(render(Primer::Alpha::Dialog::Body.new( - id: dialog_body_id, test_selector: dialog_body_id, aria: { label: title }, - classes: "FormControl-horizontalGroup--sm-vertical FormControl-horizontalGroup--center-aligned", - style: "min-height: 300px" - )) do - render(Projects::CustomFields::CustomFieldMappingForm.new(form, project_custom_field: @project_custom_field)) - end) - - concat(render(Primer::Alpha::Dialog::Footer.new(show_divider: false)) do - concat(render(Primer::ButtonComponent.new(data: { 'close-dialog-id': dialog_id })) { cancel_button_text }) - concat(render(Primer::ButtonComponent.new(scheme: :primary, type: :submit)) { submit_button_text }) - end) - end - end + render(Settings::ProjectCustomFields::ProjectCustomFieldMapping::NewProjectMappingFormComponent.new(project_mapping: @project_mapping, project_custom_field: @project_custom_field)) end %> diff --git a/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_component.rb b/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_component.rb index b6b8e71b9bb8..784dc0b8ec99 100644 --- a/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_component.rb +++ b/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_component.rb @@ -32,9 +32,6 @@ module ProjectCustomFieldMapping class NewProjectMappingComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent include OpTurbo::Streamable - options dialog_id: "settings--new-project-custom-field-mapping-component", - dialog_body_id: "settings--new-project-custom-field-mapping-body-component" - def initialize(project_mapping:, project_custom_field:, **) @project_mapping = project_mapping @project_custom_field = project_custom_field @@ -50,18 +47,6 @@ def render? def title I18n.t("projects.settings.project_custom_fields.new_project_mapping_form.add_projects") end - - def show_button_text - I18n.t("projects.settings.project_custom_fields.new_project_mapping_form.add_projects") - end - - def cancel_button_text - I18n.t("button_cancel") - end - - def submit_button_text - I18n.t("button_add") - end end end end diff --git a/modules/reporting/app/views/cost_reports/_report_menu.html.erb b/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_form_component.html.erb similarity index 55% rename from modules/reporting/app/views/cost_reports/_report_menu.html.erb rename to app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_form_component.html.erb index 51924ade9313..eaf048dfd0f1 100644 --- a/modules/reporting/app/views/cost_reports/_report_menu.html.erb +++ b/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_form_component.html.erb @@ -27,34 +27,25 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -<% items = [] %> -<% [:public, :private].each do |report_type| %> - <% queries = report_type == :public ? CostQuery.public(@project) : CostQuery.private(@project, current_user) %> - <% if queries.length > 0 %> - <% item = { - title: t(:"label_#{report_type}_report_plural"), - collapsible: true, - children: [] - } %> - - <% queries.each do |query| -%> - <% item[:children].push({ - title: query.name, - href: url_for(:controller => '/cost_reports', :action => 'show', :id => query.id) - }) %> - <% end -%> - <% items.push(item) %> - <% end %> -<% end %> - -
    -
    - <%= - angular_component_tag 'op-sidemenu', - inputs: { - title: '', - items: items - } - %> -
    -
    +<%= + component_wrapper do + primer_form_with( + class: "op-new-project-mapping-form", + model: @project_mapping, + url: link_admin_settings_project_custom_field_path(@project_custom_field), + data: { turbo: true }, + method: :post + ) do |form| + concat(render(Primer::Alpha::Dialog::Body.new( + id: DIALOG_BODY_ID, test_selector: DIALOG_BODY_ID, aria: { label: title }, + style: "min-height: 300px" + )) do + render(Projects::CustomFields::CustomFieldMappingForm.new(form, project_mapping: @project_mapping)) + end) + concat(render(Primer::Alpha::Dialog::Footer.new(show_divider: false)) do + concat(render(Primer::ButtonComponent.new(data: { 'close-dialog-id': DIALOG_ID })) { cancel_button_text }) + concat(render(Primer::ButtonComponent.new(scheme: :primary, type: :submit)) { submit_button_text }) + end) + end + end +%> diff --git a/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_form_component.rb b/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_form_component.rb new file mode 100644 index 000000000000..1693de4871fd --- /dev/null +++ b/app/components/settings/project_custom_fields/project_custom_field_mapping/new_project_mapping_form_component.rb @@ -0,0 +1,60 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Settings + module ProjectCustomFields + module ProjectCustomFieldMapping + class NewProjectMappingFormComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + include OpTurbo::Streamable + + DIALOG_ID = "settings--new-project-custom-field-mapping-component".freeze + DIALOG_BODY_ID = "settings--new-project-custom-field-mapping-body-component".freeze + + def initialize(project_mapping:, project_custom_field:) + super + @project_mapping = project_mapping + @project_custom_field = project_custom_field + end + + private + + def title + I18n.t("projects.settings.project_custom_fields.new_project_mapping_form.add_projects") + end + + def cancel_button_text + I18n.t("button_cancel") + end + + def submit_button_text + I18n.t("button_add") + end + end + end + end +end diff --git a/app/components/work_packages/share/bulk_permission_button_component.html.erb b/app/components/shares/bulk_permission_button_component.html.erb similarity index 55% rename from app/components/work_packages/share/bulk_permission_button_component.html.erb rename to app/components/shares/bulk_permission_button_component.html.erb index b1b0a5173f4f..32acee800a79 100644 --- a/app/components/work_packages/share/bulk_permission_button_component.html.erb +++ b/app/components/shares/bulk_permission_button_component.html.erb @@ -3,26 +3,26 @@ dynamic_label: true, anchor_align: :end, color: :subtle, - data: { test_selector: 'op-share-wp-bulk-update-role'})) do |menu| - menu.with_show_button(scheme: :invisible, color: :subtle, data: { 'work-packages--share--bulk-selection-target': 'bulkUpdateRoleLabel' }) do |button| + data: { test_selector: 'op-share-dialog-bulk-update-role'})) do |menu| + menu.with_show_button(scheme: :invisible, color: :subtle, data: { 'shares--bulk-selection-target': 'bulkUpdateRoleLabel' }) do |button| button.with_trailing_action_icon(icon: "triangle-down") 'Placeholder' end - options.each do |option| - menu.with_item(label: option[:label], + @available_roles.each do |role_hash| + menu.with_item(label: role_hash[:label], href: update_path, method: :patch, active: false, form_arguments: { method: :patch, name: 'role_ids[]', - value: option[:value], - data: { 'work-packages--share--bulk-selection-target': 'bulkForm bulkUpdateRoleForm', - 'role-name': option[:label], - 'test-selector': "op-share-wp-bulk-update-role-permission-#{option[:label]}" } + value: role_hash[:value], + data: { 'shares--bulk-selection-target': 'bulkForm bulkUpdateRoleForm', + 'role-name': role_hash[:label], + 'test-selector': "op-share-dialog-bulk-update-role-permission-#{role_hash[:label]}" } }) do |item| - item.with_description.with_content(option[:description]) + item.with_description.with_content(role_hash[:description]) end end end diff --git a/app/components/work_packages/share/bulk_permission_button_component.rb b/app/components/shares/bulk_permission_button_component.rb similarity index 78% rename from app/components/work_packages/share/bulk_permission_button_component.rb rename to app/components/shares/bulk_permission_button_component.rb index 1f0c2bad284c..f9a89e186737 100644 --- a/app/components/work_packages/share/bulk_permission_button_component.rb +++ b/app/components/shares/bulk_permission_button_component.rb @@ -28,20 +28,17 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -module WorkPackages - module Share - class BulkPermissionButtonComponent < ApplicationComponent - include WorkPackages::Share::Concerns::DisplayableRoles +module Shares + class BulkPermissionButtonComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + def initialize(entity:, available_roles:) + super - def initialize(work_package:) - super - - @work_package = work_package - end + @entity = entity + @available_roles = available_roles + end - def update_path - work_package_shares_bulk_path(@work_package) - end + def update_path + url_for([:bulk, @entity, Member]) end end end diff --git a/app/components/shares/bulk_selection_counter_component.html.erb b/app/components/shares/bulk_selection_counter_component.html.erb new file mode 100644 index 000000000000..9c45cb13e06b --- /dev/null +++ b/app/components/shares/bulk_selection_counter_component.html.erb @@ -0,0 +1,21 @@ +<% + concat( + render(Primer::Alpha::CheckBox.new(name: 'toggle_all', + value: nil, + label: I18n.t('sharing.label_toggle_all'), + visually_hide_label: true, + data: { 'shares--bulk-selection-target': 'toggleAll', + action: 'shares--bulk-selection#toggle' })) + ) + + concat( + render(Primer::Beta::Text.new(ml: 2, data: { 'shares--bulk-selection-target': 'sharedCounter' })) do + I18n.t('sharing.count', count:) + end + ) + + # Text contents managed by Stimulus controller + concat( + render(Primer::Beta::Text.new(ml: 2, data: { 'shares--bulk-selection-target': 'selectedCounter' })) + ) +%> diff --git a/app/components/work_packages/share/bulk_selection_counter_component.rb b/app/components/shares/bulk_selection_counter_component.rb similarity index 84% rename from app/components/work_packages/share/bulk_selection_counter_component.rb rename to app/components/shares/bulk_selection_counter_component.rb index 6dc141ba6e84..fc56f453cdeb 100644 --- a/app/components/work_packages/share/bulk_selection_counter_component.rb +++ b/app/components/shares/bulk_selection_counter_component.rb @@ -28,18 +28,16 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -module WorkPackages - module Share - class BulkSelectionCounterComponent < ApplicationComponent - def initialize(count:) - super +module Shares + class BulkSelectionCounterComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + def initialize(count:) + super - @count = count - end + @count = count + end - private + private - attr_reader :count - end + attr_reader :count end end diff --git a/app/components/work_packages/share/counter_component.html.erb b/app/components/shares/counter_component.html.erb similarity index 65% rename from app/components/work_packages/share/counter_component.html.erb rename to app/components/shares/counter_component.html.erb index 71144ef8f38a..e9281848a582 100644 --- a/app/components/work_packages/share/counter_component.html.erb +++ b/app/components/shares/counter_component.html.erb @@ -1,13 +1,13 @@ <%= - component_wrapper(data: { test_selector: 'op-share-wp-active-count'}) do + component_wrapper(data: { test_selector: 'op-share-dialog-active-count'}) do render(Primer::Box.new(display: :flex, aligns_items: :center)) do # There's no point in rendering the BulkSelectionCounterComponent even if # I'm able to manage shares if the only user that the work package is # currently shared is myself, since I'm not able to manage my own share. if sharing_manageable? && shared_with_anyone_else_other_than_myself? - render(WorkPackages::Share::BulkSelectionCounterComponent.new(count:)) + render(Shares::BulkSelectionCounterComponent.new(count:)) else - render(WorkPackages::Share::ShareCounterComponent.new(count:)) + render(Shares::ShareCounterComponent.new(count:)) end end end diff --git a/app/components/work_packages/share/counter_component.rb b/app/components/shares/counter_component.rb similarity index 65% rename from app/components/work_packages/share/counter_component.rb rename to app/components/shares/counter_component.rb index ee2b0d786da5..145ee3b8c530 100644 --- a/app/components/work_packages/share/counter_component.rb +++ b/app/components/shares/counter_component.rb @@ -28,30 +28,32 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -module WorkPackages - module Share - class CounterComponent < ApplicationComponent - include ApplicationHelper - include OpTurbo::Streamable - include OpPrimer::ComponentHelpers - include WorkPackages::Share::Concerns::Authorization - - def initialize(work_package:, count:) - super - - @work_package = work_package - @count = count - end - - private - - attr_reader :work_package, :count - - def shared_with_anyone_else_other_than_myself? - Member.of_work_package(@work_package) - .where.not(principal: User.current) - .any? - end +module Shares + class CounterComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + include ApplicationHelper + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers + + def initialize(entity:, + count:, + sharing_manageable:) + super + + @entity = entity + @count = count + @sharing_manageable = sharing_manageable + end + + private + + attr_reader :entity, :count + + def sharing_manageable? = @sharing_manageable + + def shared_with_anyone_else_other_than_myself? + Member.of_entity(@entity) + .where.not(principal: User.current) + .any? end end end diff --git a/app/components/work_packages/share/invite_user_form_component.html.erb b/app/components/shares/invite_user_form_component.html.erb similarity index 75% rename from app/components/work_packages/share/invite_user_form_component.html.erb rename to app/components/shares/invite_user_form_component.html.erb index 2cff37583ab6..5900c1a2ef47 100644 --- a/app/components/work_packages/share/invite_user_form_component.html.erb +++ b/app/components/shares/invite_user_form_component.html.erb @@ -1,39 +1,40 @@ <%= component_wrapper do - if sharing_manageable? + if @sharing_manageable primer_form_with( model: new_share, - url: url_for([@work_package, Member]), + url: url_for([@entity, Member]), data: { controller: 'user-limit ' \ - 'work-packages--share--user-selected', + 'shares--user-selected', 'application-target': 'dynamic', 'user-limit-open-seats-value': OpenProject::Enterprise.open_seats_count, - action: 'submit->work-packages--share--user-selected#ensureUsersSelected' } + action: 'submit->shares--user-selected#ensureUsersSelected' } ) do |form| grid_layout('invite-user-form', tag: :div) do |invite_form| invite_form.with_area('invitee') do - render(WorkPackages::Share::Invitee.new(form)) + render(Shares::Invitee.new(form)) end invite_form.with_area('permission') do - render(WorkPackages::Share::PermissionButtonComponent.new( + render(Shares::PermissionButtonComponent.new( share: new_share, + available_roles: @available_roles, form_arguments: { builder: form, name: "role_id" }, - data: { 'test-selector': 'op-share-wp-invite-role' }) + data: { 'test-selector': 'op-share-dialog-invite-role' }) ) end invite_form.with_area('submit') do render(Primer::Beta::Button.new(scheme: :primary, type: :submit)) do - I18n.t('work_package.sharing.share') + I18n.t('sharing.share') end end if OpenProject::Enterprise.user_limit.present? invite_form.with_area('userLimitWarning', data: { 'user-limit-target': 'limitWarning', - 'test-selector': 'op-share-wp-user-limit' }, + 'test-selector': 'op-share-dialog-user-limit' }, display: :none) do flex_layout do |user_limit_row| user_limit_row.with_column(mr: 2) do @@ -43,8 +44,9 @@ user_limit_row.with_column do render(Primer::Beta::Text.new(color: :danger)) do I18n.t( - "work_package.sharing.warning_user_limit_reached#{'_admin' if User.current.admin?}", - upgrade_url: OpenProject::Enterprise.upgrade_url + "sharing.warning_user_limit_reached#{'_admin' if User.current.admin?}", + upgrade_url: OpenProject::Enterprise.upgrade_url, + entity: @entity.model_name.human ).html_safe end end @@ -53,8 +55,8 @@ end invite_form.with_area('userSelectedWarning', - data: { 'work-packages--share--user-selected-target': 'error', - 'test-selector': 'op-share-wp-no-user-selected' }, + data: { 'shares--user-selected-target': 'error', + 'test-selector': 'op-share-dialog-no-user-selected' }, display: :none) do flex_layout do |no_selected_user_row| no_selected_user_row.with_column(mr: 2) do @@ -63,7 +65,7 @@ no_selected_user_row.with_column do render(Primer::Beta::Text.new(color: :danger)) do - I18n.t("work_package.sharing.warning_no_selected_user") + I18n.t("sharing.warning_no_selected_user", entity: @entity.model_name.human) end end end @@ -71,7 +73,7 @@ if @errors.present? invite_form.with_area('errors', - data: { 'test-selector': 'op-share-wp-error-message' }) do + data: { 'test-selector': 'op-share-dialog-error-message' }) do flex_layout do |error_rows| @errors.full_messages.each do |error_message| error_rows.with_row do @@ -94,7 +96,7 @@ end end else - render(Primer::Alpha::Banner.new(icon: :info)) { I18n.t('work_package.sharing.permissions.denied') } + render(Primer::Alpha::Banner.new(icon: :info)) { I18n.t('sharing.denied', entities: @entity.model_name.human(count: 2)) } end end %> diff --git a/app/components/shares/invite_user_form_component.rb b/app/components/shares/invite_user_form_component.rb new file mode 100644 index 000000000000..48ae8e0715ed --- /dev/null +++ b/app/components/shares/invite_user_form_component.rb @@ -0,0 +1,57 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Shares + class InviteUserFormComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + include ApplicationHelper + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers + + def initialize(entity:, + available_roles:, + sharing_manageable:, + errors: nil) + super + + @entity = entity + @available_roles = available_roles + @sharing_manageable = sharing_manageable + @errors = errors + end + + def new_share + @new_share ||= Member.new(entity: @entity, roles: [Role.new(id: default_role[:value])]) + end + + private + + def default_role + @available_roles.find { |role_hash| role_hash[:default] } || @available_roles.first + end + end +end diff --git a/app/components/work_packages/share/invite_user_form_component.sass b/app/components/shares/invite_user_form_component.sass similarity index 100% rename from app/components/work_packages/share/invite_user_form_component.sass rename to app/components/shares/invite_user_form_component.sass diff --git a/app/components/work_packages/share/modal_body_component.html.erb b/app/components/shares/modal_body_component.html.erb similarity index 65% rename from app/components/work_packages/share/modal_body_component.html.erb rename to app/components/shares/modal_body_component.html.erb index fc5bc15cce01..97de19aeae13 100644 --- a/app/components/work_packages/share/modal_body_component.html.erb +++ b/app/components/shares/modal_body_component.html.erb @@ -2,34 +2,39 @@ component_wrapper(tag: 'turbo-frame') do flex_layout(data: { turbo: true }) do |modal_content| modal_content.with_row do - render(WorkPackages::Share::InviteUserFormComponent.new(work_package: @work_package, errors: @errors)) + render(Shares::InviteUserFormComponent.new(entity: @entity, + available_roles: @available_roles, + sharing_manageable: @sharing_manageable, + errors: @errors)) end modal_content.with_row(mt: 3, - data: { 'test-selector': 'op-share-wp-active-list', - controller: 'work-packages--share--bulk-selection', + data: { 'test-selector': 'op-share-dialog-active-list', + controller: 'shares--bulk-selection', application_target: 'dynamic' }) do render(border_box_container(list_id: insert_target_modifier_id)) do |border_box| - border_box.with_header(color: :muted, data: { 'test-selector': 'op-share-wp-header' }) do - grid_layout('op-share-wp-modal-body--header', tag: :div, align_items: :center) do |header_grid| + border_box.with_header(color: :muted, data: { 'test-selector': 'op-share-dialog-header' }) do + grid_layout('op-share-dialog-modal-body--header', tag: :div, align_items: :center) do |header_grid| header_grid.with_area(:counter, tag: :div) do - render(WorkPackages::Share::CounterComponent.new(work_package: @work_package, count: @shares.size)) + render(Shares::CounterComponent.new(entity: @entity, + count: @shares.size, + sharing_manageable: @sharing_manageable)) end header_grid.with_area(:actions, tag: :div, - data: { 'work-packages--share--bulk-selection-target': 'defaultActions' }) do + data: { 'shares--bulk-selection-target': 'defaultActions' }) do flex_layout do |header_actions| header_actions.with_column(mr: 2) do render(Primer::Alpha::ActionMenu.new(anchor_align: :end, select_variant: :single, dynamic_label: true, - dynamic_label_prefix: I18n.t('work_package.sharing.filter.type'), + dynamic_label_prefix: I18n.t('sharing.filter.type'), color: :muted, - data: { 'test-selector': 'op-share-wp-filter-type' })) do |menu| - menu.with_show_button(scheme: :invisible, color: :muted, data: { 'test-selector': 'op-share-wp-filter-type-button' }) do |button| + data: { 'test-selector': 'op-share-dialog-filter-type' })) do |menu| + menu.with_show_button(scheme: :invisible, color: :muted, data: { 'test-selector': 'op-share-dialog-filter-type-button' }) do |button| button.with_trailing_action_icon(icon: "triangle-down") - I18n.t('work_package.sharing.filter.type') + I18n.t('sharing.filter.type') end type_filter_options.each do |option| menu.with_item(label: option[:label], @@ -46,19 +51,19 @@ render(Primer::Alpha::ActionMenu.new(anchor_align: :end, select_variant: :single, dynamic_label: true, - dynamic_label_prefix: I18n.t('work_package.sharing.filter.role'), + dynamic_label_prefix: I18n.t('sharing.filter.role'), color: :muted, - data: { 'test-selector': 'op-share-wp-filter-role' })) do |menu| - menu.with_show_button(scheme: :invisible, color: :muted, data: { 'test-selector': 'op-share-wp-filter-role-button' }) do |button| + data: { 'test-selector': 'op-share-dialog-filter-role' })) do |menu| + menu.with_show_button(scheme: :invisible, color: :muted, data: { 'test-selector': 'op-share-dialog-filter-role-button' }) do |button| button.with_trailing_action_icon(icon: "triangle-down") - I18n.t('work_package.sharing.filter.role') + I18n.t('sharing.filter.role') end - options.each do |option| - menu.with_item(label: option[:label], - href: filter_url(role_option: option), + @available_roles.each do |role_hash| + menu.with_item(label: role_hash[:label], + href: filter_url(role_option: role_hash), method: :get, tag: :a, - active: role_filter_option_active?(option), + active: role_filter_option_active?(role_hash), role: "menuitem") end end @@ -69,21 +74,21 @@ header_grid.with_area(:actions, tag: :div, hidden: true, # Prevent flicker on initial render - data: { 'work-packages--share--bulk-selection-target': 'bulkActions' }) do - if sharing_manageable? + data: { 'shares--bulk-selection-target': 'bulkActions' }) do + if @sharing_manageable concat( - render(WorkPackages::Share::BulkPermissionButtonComponent.new(work_package: @work_package)) + render(Shares::BulkPermissionButtonComponent.new(entity: @entity, available_roles: @available_roles)) ) concat( - form_with(url: work_package_shares_bulk_path(@work_package), + form_with(url: url_for([:bulk, @entity, Member]), method: :delete, - data: { 'work-packages--share--bulk-selection-target': 'bulkForm' }) do + data: { 'shares--bulk-selection-target': 'bulkForm' }) do render(Primer::Beta::IconButton.new(icon: "trash", type: :submit, scheme: :danger, - "aria-label": I18n.t('work_package.sharing.remove'), - test_selector: 'op-share-wp--bulk-remove')) + "aria-label": I18n.t('sharing.remove'), + test_selector: 'op-share-dialog--bulk-remove')) end ) end @@ -107,7 +112,10 @@ end else @shares.each do |share| - render(WorkPackages::Share::ShareRowComponent.new(share: share, container: border_box)) + render(Shares::ShareRowComponent.new(share: share, + available_roles: @available_roles, + sharing_manageable: @sharing_manageable, + container: border_box)) end end end diff --git a/app/components/shares/modal_body_component.rb b/app/components/shares/modal_body_component.rb new file mode 100644 index 000000000000..588f60d32016 --- /dev/null +++ b/app/components/shares/modal_body_component.rb @@ -0,0 +1,204 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Shares + class ModalBodyComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + include ApplicationHelper + include MemberHelper + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers + + attr_reader :entity, + :shares, + :available_roles, + :sharing_manageable, + :errors + + def initialize(entity:, + shares:, + available_roles:, + sharing_manageable:, + errors: nil) + super + + @entity = entity + @shares = shares + @available_roles = available_roles + @sharing_manageable = sharing_manageable + @errors = errors + end + + def self.wrapper_key + "share_list" + end + + private + + def project_scoped_entity? + entity.respond_to?(:project) + end + + def insert_target_modified? + true + end + + def insert_target_modifier_id + "op-share-dialog-active-shares" + end + + def blankslate_config # rubocop:disable Metrics/AbcSize + @blankslate_config ||= {}.tap do |config| + if params[:filters].blank? + config[:icon] = :people + config[:heading_text] = I18n.t("sharing.text_empty_state_header") + config[:description_text] = I18n.t("sharing.text_empty_state_description", entity: @entity.class.model_name.human) + else + config[:icon] = :search + config[:heading_text] = I18n.t("sharing.text_empty_search_header") + config[:description_text] = I18n.t("sharing.text_empty_search_description") + end + end + end + + def type_filter_options + if project_scoped_entity? + [ + { label: I18n.t("sharing.filter.project_member"), + value: { principal_type: "User", project_member: true } }, + { label: I18n.t("sharing.filter.not_project_member"), + value: { principal_type: "User", project_member: false } }, + { label: I18n.t("sharing.filter.project_group"), + value: { principal_type: "Group", project_member: true } }, + { label: I18n.t("sharing.filter.not_project_group"), + value: { principal_type: "Group", project_member: false } } + ] + else + [ + { label: I18n.t("sharing.filter.user"), value: { principal_type: "User" } }, + { label: I18n.t("sharing.filter.group"), value: { principal_type: "Group" } } + ] + + end + end + + def type_filter_option_active?(option) + principal_type_filter_value = current_filter_value(params[:filters], "principal_type") + project_member_filter_value = current_filter_value(params[:filters], "also_project_member") + + return false if principal_type_filter_value.nil? || project_member_filter_value.nil? + + principal_type_checked = + option[:value][:principal_type] == principal_type_filter_value + membership_selected = + option[:value][:project_member] == ActiveRecord::Type::Boolean.new.cast(project_member_filter_value) + + principal_type_checked && membership_selected + end + + def role_filter_option_active?(option) + role_filter_value = current_filter_value(params[:filters], "role_id") + + return false if role_filter_value.nil? + + selected_role = @available_roles.find { _1[:value] == option[:value] } + + selected_role[:value] == role_filter_value.to_i + end + + def filter_url(type_option: nil, role_option: nil) + return url_for([@entity, Member]) if type_option.nil? && role_option.nil? + + args = {} + filter = [] + + filter += apply_role_filter(role_option) + filter += apply_type_filter(type_option) + + args[:filters] = filter.to_json unless filter.empty? + + url_for([@entity, Member, args]) + end + + def apply_role_filter(option) + current_role_filter_value = current_filter_value(params[:filters], "role_id") + filter = [] + + if option.nil? && current_role_filter_value.present? + # When there is already a role filter set and no new value passed, we want to keep that filter + filter = role_filter_for({ value: current_role_filter_value }) + elsif option.present? && !role_filter_option_active?(option) + # Only when the passed filter option is not the currently selected one, we apply the filter + filter = role_filter_for(option) + end + + filter + end + + def role_filter_for(option) + [ + { role_id: { operator: "=", values: [option[:value]] } } + ] + end + + def apply_type_filter(option) + current_type_filter_value = current_filter_value(params[:filters], "principal_type") + current_member_filter_value = current_filter_value(params[:filters], "also_project_member") + filter = [] + + if option.nil? && current_type_filter_value.present? && current_member_filter_value.present? + # When there is already a type filter set and no new value passed, we want to keep that filter + value = { value: { principal_type: current_type_filter_value, project_member: current_member_filter_value } } + filter = type_filter_for(value) + elsif option.present? && !type_filter_option_active?(option) + # Only when the passed filter option is not the currently selected one, we apply the filter + filter = type_filter_for(option) + end + + filter + end + + def type_filter_for(option) + filter = [] + if ActiveRecord::Type::Boolean.new.cast(option[:value][:project_member]) + filter.push({ also_project_member: { operator: "=", values: [OpenProject::Database::DB_VALUE_TRUE] } }) + else + filter.push({ also_project_member: { operator: "=", values: [OpenProject::Database::DB_VALUE_FALSE] } }) + end + + filter.push({ principal_type: { operator: "=", values: [option[:value][:principal_type]] } }) + filter + end + + def current_filter_value(filters, filter_key) + return nil if filters.nil? + + given_filters = JSON.parse(filters).find { |key| key.key?(filter_key) } + given_filters ? given_filters[filter_key]["values"].first : nil + end + end +end diff --git a/app/components/work_packages/share/modal_body_component.sass b/app/components/shares/modal_body_component.sass similarity index 95% rename from app/components/work_packages/share/modal_body_component.sass rename to app/components/shares/modal_body_component.sass index 4ba5f5ee53c9..17f99bc4aab1 100644 --- a/app/components/work_packages/share/modal_body_component.sass +++ b/app/components/shares/modal_body_component.sass @@ -1,4 +1,4 @@ -.op-share-wp-modal-body +.op-share-dialog-modal-body &--user-row display: grid grid-template-columns: minmax(31px, auto) 1fr // 31px is the width needed to display a group avatar diff --git a/app/components/work_packages/share/modal_upsale_component.html.erb b/app/components/shares/modal_upsale_component.html.erb similarity index 96% rename from app/components/work_packages/share/modal_upsale_component.html.erb rename to app/components/shares/modal_upsale_component.html.erb index 2d5f3269a0d7..9f6cc4ae3e20 100644 --- a/app/components/work_packages/share/modal_upsale_component.html.erb +++ b/app/components/shares/modal_upsale_component.html.erb @@ -3,6 +3,7 @@ render Primer::Beta::Blankslate.new(border: true) do |component| component.with_visual_icon(icon: :'op-enterprise-addons', classes: 'upsale-colored') component.with_heading(tag: :h2, classes: 'upsale-colored').with_content(I18n.t(:label_enterprise_addon)) + # TODO: Generalize this text component.with_description { I18n.t('mail.sharing.work_packages.enterprise_text') } href = "#{OpenProject::Static::Links.links[:upsale][:href]}/?utm_source=unknown&utm_medium=community-edition&utm_campaign=work-package-sharing-modal" diff --git a/app/components/work_packages/share/modal_upsale_component.rb b/app/components/shares/modal_upsale_component.rb similarity index 82% rename from app/components/work_packages/share/modal_upsale_component.rb rename to app/components/shares/modal_upsale_component.rb index 4a387bec3904..933a8f061612 100644 --- a/app/components/work_packages/share/modal_upsale_component.rb +++ b/app/components/shares/modal_upsale_component.rb @@ -26,16 +26,14 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module WorkPackages - module Share - class ModalUpsaleComponent < ApplicationComponent - include ApplicationHelper - include OpTurbo::Streamable - include OpPrimer::ComponentHelpers +module Shares + class ModalUpsaleComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + include ApplicationHelper + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers - def self.wrapper_key - "work_package_share_list" - end + def self.wrapper_key + "share_list" end end end diff --git a/app/components/work_packages/share/permission_button_component.html.erb b/app/components/shares/permission_button_component.html.erb similarity index 60% rename from app/components/work_packages/share/permission_button_component.html.erb rename to app/components/shares/permission_button_component.html.erb index 076976c73e1c..6402891c82ad 100644 --- a/app/components/work_packages/share/permission_button_component.html.erb +++ b/app/components/shares/permission_button_component.html.erb @@ -4,23 +4,24 @@ dynamic_label: true, anchor_align: :end, color: :subtle }.deep_merge(@system_arguments))) do |menu| - menu.with_show_button(data: { 'work-packages--share--bulk-selection-target': 'userRowRole', + menu.with_show_button(data: { 'shares--bulk-selection-target': 'userRowRole', 'share-id': share.id, - 'active-role-name': permission_name(active_role.builtin)}) do |button| + 'active-role-name': permission_name(active_role.id)}) do |button| button.with_trailing_action_icon(icon: :"triangle-down") - permission_name(active_role.builtin) + permission_name(active_role.id) end - options.each do |option| - menu.with_item(label: option[:label], + + @available_roles.each do |role_hash| + menu.with_item(label: role_hash[:label], href: update_path, method: :patch, - active: option_active?(option), - data: { value: option[:value] }, + active: role_active?(role_hash), + data: { value: role_hash[:value] }, form_arguments: { method: :patch, - inputs: form_inputs(option[:value]) + inputs: form_inputs(role_hash[:value]) }) do |item| - item.with_description.with_content(option[:description]) + item.with_description.with_content(role_hash[:description]) end end end diff --git a/app/components/shares/permission_button_component.rb b/app/components/shares/permission_button_component.rb new file mode 100644 index 000000000000..2746241a1570 --- /dev/null +++ b/app/components/shares/permission_button_component.rb @@ -0,0 +1,84 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Shares + class PermissionButtonComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(share:, available_roles:, **system_arguments) + super + + @available_roles = available_roles + @share = share + @system_arguments = system_arguments + end + + # Switches the component to either update the share directly (by sending a PATCH to the share path) + # or be passive and work like a select inside a form. + def update_path + if share.persisted? + url_for([share.entity, share]) + end + end + + def role_active?(role_hash) + role_hash[:value] == active_role.id + end + + def wrapper_uniq_by + share.id || @system_arguments.dig(:data, :"test-selector") + end + + private + + attr_reader :share, :available_roles + + def active_role + if share.persisted? + share.roles + .merge(MemberRole.only_non_inherited) + .first + else + share.roles.first + end + end + + def permission_name(value) + available_roles.find { |role_hash| role_hash[:value] == value }[:label] + end + + def form_inputs(role_id) + [].tap do |inputs| + inputs << { name: "role_ids[]", value: role_id } + inputs << { name: "filters", value: params[:filters] } if params[:filters] + end + end + end +end diff --git a/app/components/shares/share_counter_component.html.erb b/app/components/shares/share_counter_component.html.erb new file mode 100644 index 000000000000..04f3ac8250c5 --- /dev/null +++ b/app/components/shares/share_counter_component.html.erb @@ -0,0 +1,3 @@ +<% + concat(render(Primer::Beta::Text.new) { I18n.t('sharing.count', count:) }) +%> diff --git a/app/components/work_packages/share/share_counter_component.rb b/app/components/shares/share_counter_component.rb similarity index 85% rename from app/components/work_packages/share/share_counter_component.rb rename to app/components/shares/share_counter_component.rb index 0b882c56d5dc..094b0c74dc12 100644 --- a/app/components/work_packages/share/share_counter_component.rb +++ b/app/components/shares/share_counter_component.rb @@ -28,18 +28,16 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -module WorkPackages - module Share - class ShareCounterComponent < ApplicationComponent - def initialize(count:) - super +module Shares + class ShareCounterComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + def initialize(count:) + super - @count = count - end + @count = count + end - private + private - attr_reader :count - end + attr_reader :count end end diff --git a/app/components/work_packages/share/share_row_component.html.erb b/app/components/shares/share_row_component.html.erb similarity index 63% rename from app/components/work_packages/share/share_row_component.html.erb rename to app/components/shares/share_row_component.html.erb index 539819842104..f633f52876b4 100644 --- a/app/components/work_packages/share/share_row_component.html.erb +++ b/app/components/shares/share_row_component.html.erb @@ -1,13 +1,13 @@ <%= - component_wrapper(:border_box_row, data: { 'test-selector': "op-share-wp-active-user-#{principal.id}" }) do + component_wrapper(:border_box_row, data: { 'test-selector': "op-share-dialog-active-user-#{principal.id}" }) do grid_layout(grid_css_classes, tag: :div, align_items: :center, classes: 'ellipsis') do |user_row_grid| user_row_grid.with_area(:selection, tag: :div) do if share_editable? - render(Primer::Alpha::CheckBox.new(name: "share_ids", value: share.id, label: "#{principal.name}", + render(Primer::Alpha::CheckBox.new(name: "share_ids", value: share.id, label: principal.name, visually_hide_label: true, scheme: :array, data: { - 'work-packages--share--bulk-selection-target': 'shareCheckbox', - action: 'work-packages--share--bulk-selection#refresh' + 'shares--bulk-selection-target': 'shareCheckbox', + action: 'shares--bulk-selection#refresh' })) end end @@ -17,22 +17,23 @@ end user_row_grid.with_area(:user_details, tag: :div, classes: 'ellipsis') do - render(WorkPackages::Share::UserDetailsComponent.new(share:, manager_mode: share_editable?)) + render(Shares::UserDetailsComponent.new(share:, manager_mode: share_editable?)) end if share_editable? user_row_grid.with_area(:button, tag: :div, color: :subtle) do - render(WorkPackages::Share::PermissionButtonComponent.new(share:, - data: { 'test-selector': 'op-share-wp-update-role' })) + render(Shares::PermissionButtonComponent.new(share:, + available_roles: @available_roles, + data: { 'test-selector': 'op-share-dialog-update-role' })) end user_row_grid.with_area(:remove, tag: :div) do - form_with url: url_for([work_package, share]), method: :delete do + form_with url: url_for([entity, share]), method: :delete do render(Primer::Beta::IconButton.new(icon: "trash", type: :submit, scheme: :danger, - "aria-label": I18n.t('work_package.sharing.remove'), - test_selector: 'op-share-wp--remove')) + "aria-label": I18n.t('sharing.remove'), + test_selector: 'op-share-dialog--remove')) end end end diff --git a/app/components/work_packages/share/share_row_component.rb b/app/components/shares/share_row_component.rb similarity index 50% rename from app/components/work_packages/share/share_row_component.rb rename to app/components/shares/share_row_component.rb index 16d82a331a09..8f9a1dd6e53f 100644 --- a/app/components/work_packages/share/share_row_component.rb +++ b/app/components/shares/share_row_component.rb @@ -28,53 +28,56 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -module WorkPackages - module Share - class ShareRowComponent < ApplicationComponent - include ApplicationHelper - include OpTurbo::Streamable - include OpPrimer::ComponentHelpers - include WorkPackages::Share::Concerns::Authorization +module Shares + class ShareRowComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + include ApplicationHelper + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers - def initialize(share:, - container: nil) - super + def initialize(share:, + available_roles:, + sharing_manageable:, + container: nil) + super - @share = share - @work_package = share.entity - @principal = share.principal - @container = container - end + @share = share + @entity = share.entity + @principal = share.principal + @available_roles = available_roles + @sharing_manageable = sharing_manageable + @container = container + end - def wrapper_uniq_by - share.id - end + def wrapper_uniq_by + share.id + end - private + private - attr_reader :share, :work_package, :principal, :container + attr_reader :share, :entity, :principal, :container, :available_roles - def share_editable? - @share_editable ||= User.current != share.principal && sharing_manageable? - end + def share_editable? + @share_editable ||= User.current != share.principal && sharing_manageable? + end - def grid_css_classes - if sharing_manageable? - "op-share-wp-modal-body--user-row_manageable" - else - "op-share-wp-modal-body--user-row" - end - end + def sharing_manageable? = @sharing_manageable - def select_share_checkbox_options - { - name: "share_ids", - value: share.id, - scheme: :array, - label: principal.name, - visually_hide_label: true - } + def grid_css_classes + if sharing_manageable? + "op-share-dialog-modal-body--user-row_manageable" + else + "op-share-dialog-modal-body--user-row" end end + + def select_share_checkbox_options + { + name: "share_ids", + value: share.id, + scheme: :array, + label: principal.name, + visually_hide_label: true + } + end end end diff --git a/app/components/work_packages/share/user_details_component.html.erb b/app/components/shares/user_details_component.html.erb similarity index 67% rename from app/components/work_packages/share/user_details_component.html.erb rename to app/components/shares/user_details_component.html.erb index 6ad99a059151..2af0f5a1929b 100644 --- a/app/components/work_packages/share/user_details_component.html.erb +++ b/app/components/shares/user_details_component.html.erb @@ -9,23 +9,23 @@ if manager_mode? if user_is_a_group? if project_group? - render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("work_package.sharing.user_details.project_group")} + render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("sharing.user_details.project_group")} else - render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("work_package.sharing.user_details.not_project_group")} + render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("sharing.user_details.not_project_group")} end else if user_in_non_active_status? if user.locked? concat(render(Primer::Beta::Octicon.new(icon: :lock, color: :muted, mr: 1))) - concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("work_package.sharing.user_details.locked") }) + concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("sharing.user_details.locked") }) elsif user.invited? if invite_resent? - concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("work_package.sharing.user_details.invite_resent") }) + concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("sharing.user_details.invite_resent") }) else - concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t('work_package.sharing.user_details.invited') }) + concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t('sharing.user_details.invited') }) concat( form_with(url: resend_invite_path, method: :post) do - render(Primer::Beta::Button.new(type: :submit, px: 0, scheme: :link)) { I18n.t('work_package.sharing.user_details.resend_invite') } + render(Primer::Beta::Button.new(type: :submit, px: 0, scheme: :link)) { I18n.t('sharing.user_details.resend_invite') } end ) end @@ -34,31 +34,31 @@ if part_of_a_group? if part_of_a_shared_group? if project_member? - concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("work_package.sharing.user_details.additional_privileges_project_or_group") }) + concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("sharing.user_details.additional_privileges_project_or_group") }) else - concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("work_package.sharing.user_details.additional_privileges_group") }) + concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("sharing.user_details.additional_privileges_group") }) end else if inherited_project_member? - concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("work_package.sharing.user_details.additional_privileges_project_or_group") }) + concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("sharing.user_details.additional_privileges_project_or_group") }) elsif project_member? - concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("work_package.sharing.user_details.additional_privileges_project") }) + concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("sharing.user_details.additional_privileges_project") }) else - concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("work_package.sharing.user_details.not_project_member") }) + concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("sharing.user_details.not_project_member") }) end end else if project_member? - concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("work_package.sharing.user_details.additional_privileges_project") }) + concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("sharing.user_details.additional_privileges_project") }) else - concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("work_package.sharing.user_details.not_project_member") }) + concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("sharing.user_details.not_project_member") }) end end end end else if user.invited? - concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("work_package.sharing.user_details.invited")}) + concat(render(Primer::Beta::Text.new(color: :subtle)) { I18n.t("sharing.user_details.invited")}) end end end diff --git a/app/components/shares/user_details_component.rb b/app/components/shares/user_details_component.rb new file mode 100644 index 000000000000..8f189f2b6dbf --- /dev/null +++ b/app/components/shares/user_details_component.rb @@ -0,0 +1,126 @@ +# 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 Shares + class UserDetailsComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers + + def initialize(share:, + manager_mode:, + invite_resent: false) + super + + @share = share + @user = share.principal + @manager_mode = manager_mode + @invite_resent = invite_resent + end + + private + + attr_reader :user, :share + + def manager_mode? = @manager_mode + + def invite_resent? = @invite_resent + + def wrapper_uniq_by + share.id + end + + def authoritative_work_package_role_name + @authoritative_work_package_role_name = options.find do |option| + option[:value] == share.roles.first.id + end[:label] + end + + def principal_show_path + case user + when User + user_path(user) + when Group + show_group_path(user) + else + placeholder_user_path(user) + end + end + + def resend_invite_path + url_for([:resend_invite, share.entity, share]) + end + + def user_is_a_group? + @user_is_a_group ||= user.is_a?(Group) + end + + def user_in_non_active_status? + user.locked? || user.invited? + end + + # Is a user member of a project no matter whether inherited or directly assigned + def project_member? + Member.exists?(project: share.project, + principal: user, + entity: nil) + end + + # Explicitly check whether the project membership was inherited by a group + def inherited_project_member? + Member.includes(:roles) + .references(:member_roles) + .where(project: share.project, principal: user, entity: nil) # membership in the project + .merge(MemberRole.only_inherited) # that was inherited + .any? + end + + def project_group? + user_is_a_group? && project_member? + end + + def part_of_a_shared_group? + share.member_roles.where.not(inherited_from: nil).any? + end + + def part_of_a_group? + GroupUser.where(user_id: user.id).any? + end + + def project_role_name + Member.where(project: share.project, + principal: user, + entity: nil) + .first + .roles + .first + .name + end + end +end diff --git a/app/components/work_packages/share/bulk_selection_counter_component.html.erb b/app/components/work_packages/share/bulk_selection_counter_component.html.erb deleted file mode 100644 index f3542f12e65c..000000000000 --- a/app/components/work_packages/share/bulk_selection_counter_component.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -<% - concat( - render(Primer::Alpha::CheckBox.new(name: 'toggle_all', - value: nil, - label: I18n.t('work_package.sharing.label_toggle_all'), - visually_hide_label: true, - data: { 'work-packages--share--bulk-selection-target': 'toggleAll', - action: 'work-packages--share--bulk-selection#toggle' })) - ) - - concat( - render(Primer::Beta::Text.new(ml: 2, data: { 'work-packages--share--bulk-selection-target': 'sharedCounter' })) do - I18n.t('work_package.sharing.count', count:) - end - ) - - # Text contents managed by Stimulus controller - concat( - render(Primer::Beta::Text.new(ml: 2, data: { 'work-packages--share--bulk-selection-target': 'selectedCounter' })) - ) -%> diff --git a/app/components/work_packages/share/modal_body_component.rb b/app/components/work_packages/share/modal_body_component.rb deleted file mode 100644 index d3e59c2b11c2..000000000000 --- a/app/components/work_packages/share/modal_body_component.rb +++ /dev/null @@ -1,180 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module WorkPackages - module Share - class ModalBodyComponent < ApplicationComponent - include ApplicationHelper - include MemberHelper - include OpTurbo::Streamable - include OpPrimer::ComponentHelpers - include WorkPackages::Share::Concerns::Authorization - include WorkPackages::Share::Concerns::DisplayableRoles - - def initialize(work_package:, shares:, errors: nil) - super - - @work_package = work_package - @shares = shares - @errors = errors - end - - def self.wrapper_key - "work_package_share_list" - end - - private - - def insert_target_modified? - true - end - - def insert_target_modifier_id - "op-share-wp-active-shares" - end - - def blankslate_config - @blankslate_config ||= {}.tap do |config| - if params[:filters].blank? - config[:icon] = :people - config[:heading_text] = I18n.t("work_package.sharing.text_empty_state_header") - config[:description_text] = I18n.t("work_package.sharing.text_empty_state_description") - else - config[:icon] = :search - config[:heading_text] = I18n.t("work_package.sharing.text_empty_search_header") - config[:description_text] = I18n.t("work_package.sharing.text_empty_search_description") - end - end - end - - def type_filter_options - [ - { label: I18n.t("work_package.sharing.filter.project_member"), - value: { principal_type: "User", project_member: true } }, - { label: I18n.t("work_package.sharing.filter.not_project_member"), - value: { principal_type: "User", project_member: false } }, - { label: I18n.t("work_package.sharing.filter.project_group"), - value: { principal_type: "Group", project_member: true } }, - { label: I18n.t("work_package.sharing.filter.not_project_group"), - value: { principal_type: "Group", project_member: false } } - ] - end - - def type_filter_option_active?(_option) - principal_type_filter_value = current_filter_value(params[:filters], "principal_type") - project_member_filter_value = current_filter_value(params[:filters], "also_project_member") - - return false if principal_type_filter_value.nil? || project_member_filter_value.nil? - - principal_type_checked = - _option[:value][:principal_type] == principal_type_filter_value - membership_selected = - _option[:value][:project_member] == ActiveRecord::Type::Boolean.new.cast(project_member_filter_value) - - principal_type_checked && membership_selected - end - - def role_filter_option_active?(_option) - role_filter_value = current_filter_value(params[:filters], "role_id") - - return false if role_filter_value.nil? - - find_role_ids(_option[:value]).first == role_filter_value.to_i - end - - def filter_url(type_option: nil, role_option: nil) - return url_for([@work_package, Member]) if type_option.nil? && role_option.nil? - - args = {} - filter = [] - - filter += apply_role_filter(role_option) - filter += apply_type_filter(type_option) - - args[:filters] = filter.to_json unless filter.empty? - - url_for([@work_package, Member, **args]) - end - - def apply_role_filter(_option) - current_role_filter_value = current_filter_value(params[:filters], "role_id") - filter = [] - - if _option.nil? && current_role_filter_value.present? - # When there is already a role filter set and no new value passed, we want to keep that filter - filter = role_filter_for({ value: current_role_filter_value }, builtin_role: false) - elsif _option.present? && !role_filter_option_active?(_option) - # Only when the passed filter option is not the currently selected one, we apply the filter - filter = role_filter_for(_option) - end - - filter - end - - def role_filter_for(_option, builtin_role: true) - [{ role_id: { operator: "=", values: builtin_role ? find_role_ids(_option[:value]) : [_option[:value]] } }] - end - - def apply_type_filter(_option) - current_type_filter_value = current_filter_value(params[:filters], "principal_type") - current_member_filter_value = current_filter_value(params[:filters], "also_project_member") - filter = [] - - if _option.nil? && current_type_filter_value.present? && current_member_filter_value.present? - # When there is already a type filter set and no new value passed, we want to keep that filter - value = { value: { principal_type: current_type_filter_value, project_member: current_member_filter_value } } - filter = type_filter_for(value) - elsif _option.present? && !type_filter_option_active?(_option) - # Only when the passed filter option is not the currently selected one, we apply the filter - filter = type_filter_for(_option) - end - - filter - end - - def type_filter_for(_option) - filter = [] - if ActiveRecord::Type::Boolean.new.cast(_option[:value][:project_member]) - filter.push({ also_project_member: { operator: "=", values: [OpenProject::Database::DB_VALUE_TRUE] } }) - else - filter.push({ also_project_member: { operator: "=", values: [OpenProject::Database::DB_VALUE_FALSE] } }) - end - - filter.push({ principal_type: { operator: "=", values: [_option[:value][:principal_type]] } }) - filter - end - - def current_filter_value(filters, filter_key) - return nil if filters.nil? - - given_filters = JSON.parse(filters).find { |key| key.key?(filter_key) } - given_filters ? given_filters[filter_key]["values"].first : nil - end - end - end -end diff --git a/app/components/work_packages/share/permission_button_component.rb b/app/components/work_packages/share/permission_button_component.rb deleted file mode 100644 index 07c8f242f8e1..000000000000 --- a/app/components/work_packages/share/permission_button_component.rb +++ /dev/null @@ -1,86 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module WorkPackages - module Share - class PermissionButtonComponent < ApplicationComponent - include ApplicationHelper - include OpPrimer::ComponentHelpers - include OpTurbo::Streamable - include WorkPackages::Share::Concerns::DisplayableRoles - - def initialize(share:, **system_arguments) - super - - @share = share - @system_arguments = system_arguments - end - - # Switches the component to either update the share directly (by sending a PATCH to the share path) - # or be passive and work like a select inside a form. - def update_path - if share.persisted? - url_for([share.entity, share]) - end - end - - def option_active?(option) - option[:value] == active_role.builtin - end - - def wrapper_uniq_by - share.id || @system_arguments.dig(:data, :"test-selector") - end - - private - - attr_reader :share - - def active_role - if share.persisted? - share.roles - .merge(MemberRole.only_non_inherited) - .first - else - share.roles.first - end - end - - def permission_name(value) - options.find { |option| option[:value] == value }[:label] - end - - def form_inputs(role_id) - [].tap do |inputs| - inputs << { name: "role_ids[]", value: role_id } - inputs << { name: "filters", value: params[:filters] } if params[:filters] - end - end - end - end -end diff --git a/app/components/work_packages/share/share_counter_component.html.erb b/app/components/work_packages/share/share_counter_component.html.erb deleted file mode 100644 index 855f37330382..000000000000 --- a/app/components/work_packages/share/share_counter_component.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -<% - concat(render(Primer::Beta::Text.new) { I18n.t('work_package.sharing.count', count:) }) -%> diff --git a/app/components/work_packages/share/user_details_component.rb b/app/components/work_packages/share/user_details_component.rb deleted file mode 100644 index 11c9b3fce510..000000000000 --- a/app/components/work_packages/share/user_details_component.rb +++ /dev/null @@ -1,131 +0,0 @@ -# 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 WorkPackages - module Share - # rubocop:disable OpenProject/AddPreviewForViewComponent - class UserDetailsComponent < ApplicationComponent - # rubocop:enable OpenProject/AddPreviewForViewComponent - include OpTurbo::Streamable - include OpPrimer::ComponentHelpers - include WorkPackages::Share::Concerns::DisplayableRoles - - def initialize(share:, - manager_mode: User.current.allowed_in_project?(:share_work_packages, share.project), - invite_resent: false) - super - - @share = share - @user = share.principal - @manager_mode = manager_mode - @invite_resent = invite_resent - end - - private - - attr_reader :user, :share - - def manager_mode? = @manager_mode - - def invite_resent? = @invite_resent - - def wrapper_uniq_by - share.id - end - - def authoritative_work_package_role_name - @authoritative_work_package_role_name = options.find do |option| - option[:value] == share.roles.first.builtin - end[:label] - end - - def principal_show_path - case user - when User - user_path(user) - when Group - show_group_path(user) - else - placeholder_user_path(user) - end - end - - def resend_invite_path - url_for([:resend_invite, share.entity, share]) - end - - def user_is_a_group? - @user_is_a_group ||= user.is_a?(Group) - end - - def user_in_non_active_status? - user.locked? || user.invited? - end - - # Is a user member of a project no matter whether inherited or directly assigned - def project_member? - Member.exists?(project: share.project, - principal: user, - entity: nil) - end - - # Explicitly check whether the project membership was inherited by a group - def inherited_project_member? - Member.includes(:roles) - .references(:member_roles) - .where(project: share.project, principal: user, entity: nil) # membership in the project - .merge(MemberRole.only_inherited) # that was inherited - .any? - end - - def project_group? - user_is_a_group? && project_member? - end - - def part_of_a_shared_group? - share.member_roles.where.not(inherited_from: nil).any? - end - - def part_of_a_group? - GroupUser.where(user_id: user.id).any? - end - - def project_role_name - Member.where(project: share.project, - principal: user, - entity: nil) - .first - .roles - .first - .name - end - end - end -end diff --git a/app/contracts/news/base_contract.rb b/app/contracts/news/base_contract.rb new file mode 100644 index 000000000000..7d6325f0e9eb --- /dev/null +++ b/app/contracts/news/base_contract.rb @@ -0,0 +1,50 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +class News::BaseContract < ModelContract + include Attachments::ValidateReplacements + + validate :allowed_to_manage + + def self.model + News + end + + attribute :project + attribute :title + attribute :summary + attribute :description + + def allowed_to_manage + return if model.project.nil? + + unless user.allowed_in_project?(:manage_news, model.project) + errors.add :base, :error_unauthorized + end + end +end diff --git a/app/contracts/news/create_contract.rb b/app/contracts/news/create_contract.rb new file mode 100644 index 000000000000..9a0274c59548 --- /dev/null +++ b/app/contracts/news/create_contract.rb @@ -0,0 +1,30 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +class News::CreateContract < News::BaseContract +end diff --git a/app/contracts/news/delete_contract.rb b/app/contracts/news/delete_contract.rb new file mode 100644 index 000000000000..42648cce9abc --- /dev/null +++ b/app/contracts/news/delete_contract.rb @@ -0,0 +1,31 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +class News::DeleteContract < ::DeleteContract + delete_permission :manage_news +end diff --git a/app/contracts/news/update_contract.rb b/app/contracts/news/update_contract.rb new file mode 100644 index 000000000000..04bb5fef4bf0 --- /dev/null +++ b/app/contracts/news/update_contract.rb @@ -0,0 +1,30 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +class News::UpdateContract < News::BaseContract +end diff --git a/app/contracts/queries/projects/project_queries/base_contract.rb b/app/contracts/queries/projects/project_queries/base_contract.rb index da00abf3366a..a98d9fa807c3 100644 --- a/app/contracts/queries/projects/project_queries/base_contract.rb +++ b/app/contracts/queries/projects/project_queries/base_contract.rb @@ -34,7 +34,7 @@ class BaseContract < ::ModelContract attribute :orders def self.model - Queries::Projects::ProjectQuery + ProjectQuery end validates :name, diff --git a/app/contracts/work_package_members/base_contract.rb b/app/contracts/shares/base_contract.rb similarity index 89% rename from app/contracts/work_package_members/base_contract.rb rename to app/contracts/shares/base_contract.rb index 792865969400..020973c6dd03 100644 --- a/app/contracts/work_package_members/base_contract.rb +++ b/app/contracts/shares/base_contract.rb @@ -26,17 +26,13 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -module WorkPackageMembers +module Shares class BaseContract < ::ModelContract - delegate :project, - to: :model - attribute :roles validate :user_allowed_to_manage validate :role_grantable validate :single_non_inherited_role - validate :project_set validate :entity_set attribute_alias(:user_id, :principal) @@ -50,7 +46,7 @@ def user_allowed_to_manage end def user_allowed_to_manage? - user.allowed_in_project?(:share_work_packages, model.project) + raise NotImplementedError, "Must be overridden by subclass" end def single_non_inherited_role @@ -58,11 +54,7 @@ def single_non_inherited_role end def role_grantable - errors.add(:roles, :ungrantable) unless active_roles.all? { _1.is_a?(WorkPackageRole) } - end - - def project_set - errors.add(:project, :blank) if project.nil? + errors.add(:roles, :ungrantable) unless active_roles.all? { _1.is_a?(assignable_role_class) } end def active_roles @@ -80,5 +72,9 @@ def active_member_roles def entity_set errors.add(:entity, :blank) if entity_id.nil? end + + def assignable_role_class + raise NotImplementedError, "Must be overridden by subclass" + end end end diff --git a/app/contracts/work_package_members/create_contract.rb b/app/contracts/shares/create_contract.rb similarity index 98% rename from app/contracts/work_package_members/create_contract.rb rename to app/contracts/shares/create_contract.rb index c21ce60ef1a2..190af5d6ea5e 100644 --- a/app/contracts/work_package_members/create_contract.rb +++ b/app/contracts/shares/create_contract.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -module WorkPackageMembers +module Shares class CreateContract < BaseContract attribute :principal attribute :entity_id diff --git a/app/contracts/work_package_members/delete_contract.rb b/app/contracts/shares/delete_contract.rb similarity index 95% rename from app/contracts/work_package_members/delete_contract.rb rename to app/contracts/shares/delete_contract.rb index 41726467fa50..165815bb1fb7 100644 --- a/app/contracts/work_package_members/delete_contract.rb +++ b/app/contracts/shares/delete_contract.rb @@ -26,10 +26,8 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -module WorkPackageMembers +module Shares class DeleteContract < ::DeleteContract - delete_permission :share_work_packages - validate :member_is_deletable private diff --git a/app/contracts/work_package_members/update_contract.rb b/app/contracts/shares/update_contract.rb similarity index 98% rename from app/contracts/work_package_members/update_contract.rb rename to app/contracts/shares/update_contract.rb index 785ec1030ff4..355a739d558c 100644 --- a/app/contracts/work_package_members/update_contract.rb +++ b/app/contracts/shares/update_contract.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -module WorkPackageMembers +module Shares class UpdateContract < BaseContract attribute :principal, writable: false diff --git a/app/contracts/shares/work_packages/base_extension.rb b/app/contracts/shares/work_packages/base_extension.rb new file mode 100644 index 000000000000..6fa99c6a5300 --- /dev/null +++ b/app/contracts/shares/work_packages/base_extension.rb @@ -0,0 +1,54 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-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 Shares + module WorkPackages + module BaseExtension + extend ActiveSupport::Concern + + included do + delegate :project, to: :model + validate :project_set + end + + private + + def user_allowed_to_manage? + user.allowed_in_project?(:share_work_packages, project) + end + + def assignable_role_class + WorkPackageRole + end + + def project_set + errors.add(:project, :blank) if project.nil? + end + end + end +end diff --git a/app/contracts/shares/work_packages/create_contract.rb b/app/contracts/shares/work_packages/create_contract.rb new file mode 100644 index 000000000000..eced07e71743 --- /dev/null +++ b/app/contracts/shares/work_packages/create_contract.rb @@ -0,0 +1,35 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-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 Shares + module WorkPackages + class CreateContract < Shares::CreateContract + include Shares::WorkPackages::BaseExtension + end + end +end diff --git a/app/contracts/shares/work_packages/delete_contract.rb b/app/contracts/shares/work_packages/delete_contract.rb new file mode 100644 index 000000000000..a2ac75399add --- /dev/null +++ b/app/contracts/shares/work_packages/delete_contract.rb @@ -0,0 +1,37 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-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 Shares + module WorkPackages + class DeleteContract < Shares::DeleteContract + # DeleteContract has its own permission check and does not care about the role class, + # so we do not need to include the BaseExtension here. + delete_permission :share_work_packages + end + end +end diff --git a/app/contracts/shares/work_packages/update_contract.rb b/app/contracts/shares/work_packages/update_contract.rb new file mode 100644 index 000000000000..f38d818cda01 --- /dev/null +++ b/app/contracts/shares/work_packages/update_contract.rb @@ -0,0 +1,35 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-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 Shares + module WorkPackages + class UpdateContract < Shares::UpdateContract + include Shares::WorkPackages::BaseExtension + end + end +end diff --git a/app/controllers/admin/settings/project_custom_fields_controller.rb b/app/controllers/admin/settings/project_custom_fields_controller.rb index 59d366d3d9a9..8f37bb49c650 100644 --- a/app/controllers/admin/settings/project_custom_fields_controller.rb +++ b/app/controllers/admin/settings/project_custom_fields_controller.rb @@ -30,6 +30,7 @@ module Admin::Settings class ProjectCustomFieldsController < ::Admin::SettingsController include CustomFields::SharedActions include OpTurbo::ComponentStream + include OpTurbo::DialogStreamHelper include ApplicationComponentStreams include FlashMessagesOutputSafetyHelper include Admin::Settings::ProjectCustomFields::ComponentStreams @@ -39,7 +40,8 @@ class ProjectCustomFieldsController < ::Admin::SettingsController # rubocop:disable Rails/LexicallyScopedActionFilter before_action :set_sections, only: %i[show index edit update move drop] before_action :find_custom_field, - only: %i(show edit project_mappings link unlink update destroy delete_option reorder_alphabetical move drop) + only: %i(show edit project_mappings new_link link unlink update destroy delete_option reorder_alphabetical + move drop) before_action :prepare_custom_option_position, only: %i(update create) before_action :find_custom_option, only: :delete_option before_action :project_custom_field_mappings_query, only: %i[project_mappings unlink] @@ -69,8 +71,14 @@ def new def edit; end - def project_mappings + def project_mappings; end + + def new_link @project_mapping = ProjectCustomFieldProjectMapping.new(project_custom_field: @custom_field) + respond_with_dialog Settings::ProjectCustomFields::ProjectCustomFieldMapping::NewProjectMappingComponent.new( + project_mapping: @project_mapping, + project_custom_field: @custom_field + ) end def link @@ -148,12 +156,6 @@ def destroy private def render_project_list(url_for_action: action_name) - update_via_turbo_stream( - component: Settings::ProjectCustomFields::ProjectCustomFieldMapping::NewProjectMappingComponent.new( - project_mapping: ProjectCustomFieldProjectMapping.new(project_custom_field: @custom_field), - project_custom_field: @custom_field - ) - ) update_via_turbo_stream( component: Settings::ProjectCustomFields::ProjectCustomFieldMapping::TableComponent.new( query: project_custom_field_mappings_query, @@ -163,7 +165,7 @@ def render_project_list(url_for_action: action_name) end def project_custom_field_mappings_query - @project_custom_field_mappings_query = Queries::Projects::ProjectQuery.new( + @project_custom_field_mappings_query = ProjectQuery.new( name: "project-custom-field-mappings-#{@custom_field.id}" ) do |query| query.where(:available_project_attributes, "=", [@custom_field.id]) @@ -196,10 +198,23 @@ def find_unlink_project_custom_field_mapping end def find_custom_field_projects_to_link - @projects = Project.find(params.to_unsafe_h[:project_custom_field_project_mapping][:project_ids]) + project_ids = params.to_unsafe_h[:project_custom_field_project_mapping][:project_ids] + if project_ids.present? + @projects = Project.find(project_ids) + else + project_mapping = ProjectCustomFieldProjectMapping.new(project_custom_field: @custom_field) + project_mapping.errors.add(:project_ids, :blank) + component = Settings::ProjectCustomFields::ProjectCustomFieldMapping::NewProjectMappingFormComponent.new( + project_mapping:, + project_custom_field: @custom_field + ) + update_via_turbo_stream(component:, status: :bad_request) + respond_with_turbo_streams + false + end rescue ActiveRecord::RecordNotFound update_flash_message_via_turbo_stream( - message: t(:notice_file_not_found), full: true, dismiss_scheme: :hide, scheme: :danger + message: t(:notice_project_not_found), full: true, dismiss_scheme: :hide, scheme: :danger ) render_project_list diff --git a/app/controllers/concerns/member_helper.rb b/app/controllers/concerns/member_helper.rb index 70c7a09a5561..8eb65831fd92 100644 --- a/app/controllers/concerns/member_helper.rb +++ b/app/controllers/concerns/member_helper.rb @@ -29,12 +29,6 @@ module MemberHelper module_function - def find_role_ids(builtin_value) - # Role has a left join on permissions included leading to multiple ids being returned which - # is why we unscope. - WorkPackageRole.unscoped.where(builtin: builtin_value).pluck(:id) - end - def find_or_create_users(send_notification: true) @send_notification = send_notification diff --git a/app/controllers/concerns/shares/work_packages/authorization.rb b/app/controllers/concerns/shares/work_packages/authorization.rb new file mode 100644 index 000000000000..32b4bf5f34e0 --- /dev/null +++ b/app/controllers/concerns/shares/work_packages/authorization.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ + +module Shares + module WorkPackages + module Authorization + extend ActiveSupport::Concern + + included do + def sharing_manageable? + # TODO: Fix this to check based on the entity + case @entity + when WorkPackage + User.current.allowed_in_project?(:share_work_packages, @entity.project) + else + raise ArgumentError, <<~ERROR + Checking sharing capabilities for an unsupported entity: + - #{@entity.class} + ERROR + end + end + end + end + end +end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 778ddd6082c6..85384f3b0cab 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -155,7 +155,8 @@ def group_members end def visible_group_members? - current_user.allowed_in_any_project?(:manage_members) || + current_user.admin? || + current_user.allowed_in_any_project?(:manage_members) || Group.in_project(Project.allowed_to(current_user, :view_members)).exists? end diff --git a/app/controllers/members/menus_controller.rb b/app/controllers/members/menus_controller.rb index 75be7ba2c8c2..edfaec250776 100644 --- a/app/controllers/members/menus_controller.rb +++ b/app/controllers/members/menus_controller.rb @@ -27,13 +27,11 @@ #++ module Members class MenusController < ApplicationController - include Menus::MembersHelper - before_action :find_project_by_project_id, :authorize def show - @sidebar_menu_items = first_level_menu_items + @sidebar_menu_items = Members::Menu.new(project: @project, params:).menu_items render layout: nil end end diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index 4b64724d3fe5..0a0a3179c56f 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -86,8 +86,8 @@ def destroy_by_principal principal = Principal.find(params[:principal_id]) service_call = Members::DeleteByPrincipalService - .new(user: current_user, project: @project, principal:) - .call(params.permit(:project, :work_package_shares_role_id)) + .new(user: current_user, project: @project, principal:) + .call(params.permit(:project, :work_package_shares_role_id)) if service_call.success? flash[:notice] = I18n.t(:notice_member_removed, user: principal.name) @@ -144,8 +144,8 @@ def members_table_options(roles) available_roles: roles, authorize_update: authorize_for("members", :update), authorize_delete: authorize_for("members", :destroy), - authorize_work_package_shares_view: authorize_for("work_packages/shares", :update), - authorize_work_package_shares_delete: authorize_for("work_packages/shares/bulk", :destroy), + authorize_work_package_shares_view: authorize_for("shares", :update), + authorize_work_package_shares_delete: authorize_for("shares", :bulk_destroy), authorize_manage_user: current_user.allowed_globally?(:manage_user), is_filtered: Members::UserFilterComponent.filtered?(params), shared_role_name: diff --git a/app/controllers/news_controller.rb b/app/controllers/news_controller.rb index 27e9d57ef12e..31dae3297a4a 100644 --- a/app/controllers/news_controller.rb +++ b/app/controllers/news_controller.rb @@ -73,29 +73,44 @@ def new def edit; end def create - @news = News.new(project: @project, author: User.current) - @news.attributes = permitted_params.news - if @news.save + call = News::CreateService + .new(user: current_user) + .call(permitted_params.news.merge(project: @project)) + + if call.success? flash[:notice] = I18n.t(:notice_successful_create) redirect_to controller: "/news", action: "index", project_id: @project else + @news = call.result render action: "new" end end def update - @news.attributes = permitted_params.news - if @news.save + call = News::UpdateService + .new(model: @news, user: current_user) + .call(permitted_params.news.merge(project: @project)) + + if call.success? flash[:notice] = I18n.t(:notice_successful_update) redirect_to action: "show", id: @news else + @news = call.result render action: "edit" end end def destroy - @news.destroy - flash[:notice] = I18n.t(:notice_successful_delete) + call = News::DeleteService + .new(model: @news, user: current_user) + .call + + if call.success? + flash[:notice] = I18n.t(:notice_successful_delete) + else + call.apply_flash_message!(flash) + end + redirect_to action: "index", project_id: @project end diff --git a/app/controllers/projects/menus_controller.rb b/app/controllers/projects/menus_controller.rb index cdecdf19bbdf..e339bbc5a180 100644 --- a/app/controllers/projects/menus_controller.rb +++ b/app/controllers/projects/menus_controller.rb @@ -32,9 +32,8 @@ class MenusController < ApplicationController no_authorization_required! :show def show - projects_menu = Menus::Projects.new(controller_path: params[:controller_path], params:, current_user:) - - @sidebar_menu_items = projects_menu.first_level_menu_items + projects_menu = Projects::Menu.new(controller_path: params[:controller_path], params:, current_user:) + @sidebar_menu_items = projects_menu.menu_items render layout: nil end diff --git a/app/controllers/projects/queries_controller.rb b/app/controllers/projects/queries_controller.rb index 04914ac7d6ab..abbc67b2c4fc 100644 --- a/app/controllers/projects/queries_controller.rb +++ b/app/controllers/projects/queries_controller.rb @@ -113,6 +113,6 @@ def render_result(service_call, success_i18n_key:, error_i18n_key:) # rubocop:di end def find_query - @query = Queries::Projects::ProjectQuery.visible(current_user).find(params[:id]) + @query = ProjectQuery.visible(current_user).find(params[:id]) end end diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb new file mode 100644 index 000000000000..0e8a174e3d31 --- /dev/null +++ b/app/controllers/shares_controller.rb @@ -0,0 +1,376 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-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 SharesController < ApplicationController + include OpTurbo::ComponentStream + include Shares::WorkPackages::Authorization + include MemberHelper + + before_action :load_entity + before_action :load_shares, only: %i[index] + before_action :load_selected_shares, only: %i[bulk_update bulk_destroy] + before_action :load_share, only: %i[destroy update resend_invite] + before_action :authorize + before_action :enterprise_check, only: %i[index] + + def index + unless @query.valid? + flash.now[:error] = query.errors.full_messages + end + + render Shares::ModalBodyComponent.new(entity: @entity, + shares: @shares, + errors: @errors, + sharing_manageable: sharing_manageable?, + available_roles:), layout: nil + end + + def create # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity + overall_result = [] + @errors = ActiveModel::Errors.new(self) + + find_or_create_users(send_notification: false) do |member_params| + user = User.find_by(id: member_params[:user_id]) + if user.present? && user.locked? + @errors.add(:base, I18n.t("sharing.warning_locked_user", user: user.name)) + else + service_call = create_or_update_share(member_params[:user_id], [params[:member][:role_id]]) + overall_result.push(service_call) + end + end + + @new_shares = overall_result.map(&:result).reverse + + if overall_result.present? + # In case the number of newly added shares is equal to the whole number of shares, + # we have to render the whole modal again to get rid of the blankslate + if current_visible_member_count > 1 && @new_shares.size < current_visible_member_count + respond_with_prepend_shares + else + respond_with_replace_modal + end + else + respond_with_new_invite_form + end + end + + def update + create_or_update_share(@share.principal.id, params[:role_ids]) + + load_shares + + if @shares.empty? + respond_with_replace_modal + elsif @shares.include?(@share) + respond_with_update_permission_button + else + respond_with_remove_share + end + end + + def destroy + destroy_share(@share) + + if current_visible_member_count.zero? + respond_with_replace_modal + else + respond_with_remove_share + end + end + + def resend_invite + OpenProject::Notifications.send(OpenProject::Events::WORK_PACKAGE_SHARED, + work_package_member: @share, + send_notifications: true) + + respond_with_update_user_details + end + + def bulk_update + @selected_shares.each { |share| create_or_update_share(share.principal.id, params[:role_ids]) } + + respond_with_bulk_updated_permission_buttons + end + + def bulk_destroy + @selected_shares.each { |share| destroy_share(share) } + + if current_visible_member_count.zero? + respond_with_replace_modal + else + respond_with_bulk_removed_shares + end + end + + private + + def enterprise_check + return if EnterpriseToken.allows_to?(:work_package_sharing) + + render Shares::ModalUpsaleComponent.new + end + + def destroy_share(share) + Shares::DeleteService + .new(user: current_user, model: share, contract_class: sharing_contract_scope::DeleteContract) + .call + end + + def create_or_update_share(user_id, role_ids) + Shares::CreateOrUpdateService.new( + user: current_user, + create_contract_class: sharing_contract_scope::CreateContract, + update_contract_class: sharing_contract_scope::UpdateContract + ) + .call(entity: @entity, user_id:, role_ids:) + end + + def respond_with_replace_modal + replace_via_turbo_stream( + component: Shares::ModalBodyComponent.new( + entity: @entity, + available_roles:, + shares: @new_shares || load_shares, + sharing_manageable: sharing_manageable?, + errors: @errors + ) + ) + + respond_with_turbo_streams + end + + def respond_with_prepend_shares # rubocop:disable Metrics/AbcSize + replace_via_turbo_stream( + component: Shares::InviteUserFormComponent.new( + entity: @entity, + available_roles:, + sharing_manageable: sharing_manageable?, + errors: @errors + ) + ) + + update_via_turbo_stream( + component: Shares::CounterComponent.new( + entity: @entity, + count: current_visible_member_count, + sharing_manageable: sharing_manageable? + ) + ) + + @new_shares.each do |share| + prepend_via_turbo_stream( + component: Shares::ShareRowComponent.new( + share:, + available_roles:, + sharing_manageable: sharing_manageable? + ), + target_component: Shares::ModalBodyComponent.new( + entity: @entity, + available_roles:, + sharing_manageable: sharing_manageable?, + shares: load_shares, + errors: @errors + ) + ) + end + + respond_with_turbo_streams + end + + def respond_with_new_invite_form + replace_via_turbo_stream( + component: Shares::InviteUserFormComponent.new( + entity: @entity, + available_roles:, + sharing_manageable: sharing_manageable?, + errors: @errors + ) + ) + + respond_with_turbo_streams + end + + def respond_with_update_permission_button + replace_via_turbo_stream( + component: Shares::PermissionButtonComponent.new( + share: @share, + available_roles:, + data: { "test-selector": "op-share-dialog-update-role" } + ) + ) + + respond_with_turbo_streams + end + + def respond_with_remove_share + remove_via_turbo_stream( + component: Shares::ShareRowComponent.new( + share: @share, + available_roles:, + sharing_manageable: sharing_manageable? + ) + ) + update_via_turbo_stream( + component: Shares::CounterComponent.new( + entity: @entity, + count: current_visible_member_count, + sharing_manageable: sharing_manageable? + ) + ) + + respond_with_turbo_streams + end + + def respond_with_update_user_details + update_via_turbo_stream( + component: Shares::UserDetailsComponent.new( + share: @share, + manager_mode: sharing_manageable?, + invite_resent: true + ) + ) + + respond_with_turbo_streams + end + + def respond_with_bulk_updated_permission_buttons + @selected_shares.each do |share| + replace_via_turbo_stream( + component: Shares::PermissionButtonComponent.new( + share:, + available_roles:, + data: { "test-selector": "op-share-dialog-update-role" } + ) + ) + end + + respond_with_turbo_streams + end + + def respond_with_bulk_removed_shares + @selected_shares.each do |share| + remove_via_turbo_stream( + component: Shares::ShareRowComponent.new( + share:, + available_roles:, + sharing_manageable: sharing_manageable? + ) + ) + end + + update_via_turbo_stream( + component: Shares::CounterComponent.new( + entity: @entity, + count: current_visible_member_count, + sharing_manageable: sharing_manageable? + ) + ) + + respond_with_turbo_streams + end + + def load_entity + @entity = if params["work_package_id"] + WorkPackage.visible.find(params["work_package_id"]) + # TODO: Add support for other entities + else + raise ArgumentError, <<~ERROR + Nested the SharesController under an entity controller that is not yet configured to support sharing. + Edit the SharesController#load_entity method to load the entity from the correct parent. + ERROR + end + + if @entity.respond_to?(:project) + @project = @entity.project + end + end + + def load_share + @share = @entity.members.find(params[:id]) + end + + def current_visible_member_count + @current_visible_member_count ||= load_shares.size + end + + def load_query + return @query if defined?(@query) + + @query = ParamsToQueryService + .new(Member, current_user, query_class: Queries::Members::EntityMemberQuery) + .call(params) + + # Set default filter on the entity + @query.where("entity_id", "=", @entity.id) + @query.where("entity_type", "=", @entity.class.name) + if @project + @query.where("project_id", "=", @project.id) + end + + @query.order(name: :asc) unless params[:sortBy] + + @query + end + + def load_shares + @shares = load_query.results + end + + def load_selected_shares + @selected_shares = Member.includes(:principal) + .of_entity(@entity) + .where(id: params[:share_ids]) + end + + def available_roles + @available_roles ||= if @entity.is_a?(WorkPackage) + role_mapping = WorkPackageRole.unscoped.pluck(:builtin, :id).to_h + + [ + { label: I18n.t("work_package.permissions.edit"), + value: role_mapping[Role::BUILTIN_WORK_PACKAGE_EDITOR], + description: I18n.t("work_package.permissions.edit_description") }, + { label: I18n.t("work_package.permissions.comment"), + value: role_mapping[Role::BUILTIN_WORK_PACKAGE_COMMENTER], + description: I18n.t("work_package.permissions.comment_description") }, + { label: I18n.t("work_package.permissions.view"), + value: role_mapping[Role::BUILTIN_WORK_PACKAGE_VIEWER], + description: I18n.t("work_package.permissions.view_description"), + default: true } + ] + else + [] + end + end + + def sharing_contract_scope + if @entity.is_a?(WorkPackage) + Shares::WorkPackages + end + end +end diff --git a/app/controllers/work_packages/moves_controller.rb b/app/controllers/work_packages/moves_controller.rb index 42c65161fafa..a9bbe6cd8e96 100644 --- a/app/controllers/work_packages/moves_controller.rb +++ b/app/controllers/work_packages/moves_controller.rb @@ -60,7 +60,7 @@ def within_frontend_treshold? # rubocop:disable Metrics/AbcSize def perform_in_frontend call = job_class - .perform_now(**job_args) + .perform_now(**job_args) if call.success? && @work_packages.any? flash[:notice] = call.message @@ -70,6 +70,7 @@ def perform_in_frontend redirect_back_or_default(project_work_packages_path(@project)) end end + # rubocop:enable Metrics/AbcSize def perform_in_background @@ -114,13 +115,31 @@ def prepare_for_work_package_move @allowed_projects = WorkPackage.allowed_target_projects_on_move(current_user) @target_project = @allowed_projects.detect { |p| p.id.to_s == params[:new_project_id].to_s } if params[:new_project_id] @target_project ||= @project - @types = @target_project.types + @types = @target_project.types.order(:position) @target_type = @types.find { |t| t.id.to_s == params[:new_type_id].to_s } + @unavailable_type_in_target_project = set_unavailable_type_in_target_project @available_versions = @target_project.assignable_versions @available_statuses = Workflow.available_statuses(@project) @notes = params[:notes] || "" end + def set_unavailable_type_in_target_project + if @target_project == @project + false + elsif @target_type.nil? + hierarchies = WorkPackageHierarchy + .includes(:ancestor) + .where(ancestor_id: @work_packages.select(:id)) + Type.where(id: hierarchies.map { _1.ancestor.type_id }) + .select("distinct id") + .pluck(:id) + .difference(@types.pluck(:id)) + .any? + else + @types.exclude?(@target_type) + end + end + def attributes_for_create permitted_params .move_work_package diff --git a/app/controllers/work_packages/shares/bulk_controller.rb b/app/controllers/work_packages/shares/bulk_controller.rb deleted file mode 100644 index 79db00e72854..000000000000 --- a/app/controllers/work_packages/shares/bulk_controller.rb +++ /dev/null @@ -1,134 +0,0 @@ -# frozen_string_literal: true - -# -- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2010-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 WorkPackages::Shares::BulkController < ApplicationController - include OpTurbo::ComponentStream - include MemberHelper - - before_action :find_work_package - before_action :find_selected_shares - before_action :find_role_ids_from_params, only: :update - before_action :find_project - before_action :authorize - - def update - @selected_shares.each do |share| - WorkPackageMembers::CreateOrUpdateService - .new(user: current_user) - .call(entity: @work_package, - user_id: share.principal.id, - role_ids: @role_ids).result - end - - respond_with_update_permission_buttons - end - - def destroy - @selected_shares.each do |share| - WorkPackageMembers::DeleteService - .new(user: current_user, model: share) - .call - end - - if current_visible_member_count.zero? - respond_with_replace_modal - else - respond_with_remove_shares - end - end - - private - - def respond_with_update_permission_buttons - @selected_shares.each do |share| - replace_via_turbo_stream( - component: WorkPackages::Share::PermissionButtonComponent.new(share:, - data: { "test-selector": "op-share-wp-update-role" }) - ) - end - - respond_with_turbo_streams - end - - def respond_with_replace_modal - replace_via_turbo_stream( - component: WorkPackages::Share::ModalBodyComponent.new(work_package: @work_package, shares: find_shares) - ) - - respond_with_turbo_streams - end - - def respond_with_remove_shares - @selected_shares.each do |share| - remove_via_turbo_stream( - component: WorkPackages::Share::ShareRowComponent.new(share:) - ) - end - - update_via_turbo_stream( - component: WorkPackages::Share::CounterComponent.new(work_package: @work_package, count: current_visible_member_count) - ) - - respond_with_turbo_streams - end - - def find_work_package - @work_package = WorkPackage.find(params[:work_package_id]) - end - - def find_project - @project = @work_package.project - end - - def find_shares - @shares = Member.includes(:principal, :member_roles) - .references(:member_roles) - .of_work_package(@work_package) - .merge(MemberRole.only_non_inherited) - end - - def find_selected_shares - @selected_shares = Member.includes(:principal) - .of_work_package(@work_package) - .where(id: params[:share_ids]) - end - - def find_role_ids_from_params - @role_ids = find_role_ids(params[:role_ids]) - end - - def current_visible_member_count - @current_visible_member_count ||= Member - .joins(:member_roles) - .of_work_package(@work_package) - .merge(MemberRole.only_non_inherited) - .size - end -end diff --git a/app/controllers/work_packages/shares_controller.rb b/app/controllers/work_packages/shares_controller.rb deleted file mode 100644 index 930f2fc8dfc6..000000000000 --- a/app/controllers/work_packages/shares_controller.rb +++ /dev/null @@ -1,238 +0,0 @@ -# -- copyright -# OpenProject is an open source project management software. -# Copyright (C) 2010-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 WorkPackages::SharesController < ApplicationController - include OpTurbo::ComponentStream - include MemberHelper - - before_action :find_work_package, only: %i[index create destroy update resend_invite] - before_action :find_share, only: %i[destroy update resend_invite] - before_action :find_project - before_action :authorize - before_action :enterprise_check, only: %i[index] - - def index - query = load_query - - unless query.valid? - flash.now[:error] = query.errors.full_messages - end - - @shares = load_shares query - - render WorkPackages::Share::ModalBodyComponent.new(work_package: @work_package, shares: @shares, errors: @errors), layout: nil - end - - def create - overall_result = [] - @errors = ActiveModel::Errors.new(self) - - find_or_create_users(send_notification: false) do |member_params| - user = User.find_by(id: member_params[:user_id]) - if user.present? && user.locked? - @errors.add(:base, I18n.t("work_package.sharing.warning_locked_user", user: user.name)) - else - service_call = WorkPackageMembers::CreateOrUpdateService - .new(user: current_user) - .call(entity: @work_package, - user_id: member_params[:user_id], - role_ids: find_role_ids(params[:member][:role_id])) - - overall_result.push(service_call) - end - end - - @new_shares = overall_result.map(&:result).reverse - - if overall_result.present? - # In case the number of newly added shares is equal to the whole number of shares, - # we have to render the whole modal again to get rid of the blankslate - if current_visible_member_count > 1 && @new_shares.size < current_visible_member_count - respond_with_prepend_shares - else - respond_with_replace_modal - end - else - respond_with_new_invite_form - end - end - - def update - WorkPackageMembers::UpdateService - .new(user: current_user, model: @share) - .call(role_ids: find_role_ids(params[:role_ids])) - - find_shares - - if @shares.empty? - respond_with_replace_modal - elsif @shares.include?(@share) - respond_with_update_permission_button - else - respond_with_remove_share - end - end - - def destroy - WorkPackageMembers::DeleteService - .new(user: current_user, model: @share) - .call - - if current_visible_member_count.zero? - respond_with_replace_modal - else - respond_with_remove_share - end - end - - def resend_invite - OpenProject::Notifications.send(OpenProject::Events::WORK_PACKAGE_SHARED, - work_package_member: @share, - send_notifications: true) - - respond_with_update_user_details - end - - private - - def enterprise_check - return if EnterpriseToken.allows_to?(:work_package_sharing) - - render WorkPackages::Share::ModalUpsaleComponent.new - end - - def respond_with_replace_modal - replace_via_turbo_stream( - component: WorkPackages::Share::ModalBodyComponent.new(work_package: @work_package, - shares: @new_shares || find_shares, - errors: @errors) - ) - - respond_with_turbo_streams - end - - def respond_with_prepend_shares - replace_via_turbo_stream( - component: WorkPackages::Share::InviteUserFormComponent.new(work_package: @work_package, errors: @errors) - ) - - update_via_turbo_stream( - component: WorkPackages::Share::CounterComponent.new(work_package: @work_package, count: current_visible_member_count) - ) - - @new_shares.each do |share| - prepend_via_turbo_stream( - component: WorkPackages::Share::ShareRowComponent.new(share:), - target_component: WorkPackages::Share::ModalBodyComponent.new(work_package: @work_package, - shares: find_shares, - errors: @errors) - ) - end - - respond_with_turbo_streams - end - - def respond_with_new_invite_form - replace_via_turbo_stream( - component: WorkPackages::Share::InviteUserFormComponent.new(work_package: @work_package, errors: @errors) - ) - - respond_with_turbo_streams - end - - def respond_with_update_permission_button - replace_via_turbo_stream( - component: WorkPackages::Share::PermissionButtonComponent.new(share: @share, - data: { "test-selector": "op-share-wp-update-role" }) - ) - - respond_with_turbo_streams - end - - def respond_with_remove_share - remove_via_turbo_stream( - component: WorkPackages::Share::ShareRowComponent.new(share: @share) - ) - - update_via_turbo_stream( - component: WorkPackages::Share::CounterComponent.new(work_package: @work_package, count: current_visible_member_count) - ) - - respond_with_turbo_streams - end - - def respond_with_update_user_details - update_via_turbo_stream( - component: WorkPackages::Share::UserDetailsComponent.new(share: @share, - invite_resent: true) - ) - - respond_with_turbo_streams - end - - def find_work_package - @work_package = WorkPackage.find(params[:work_package_id]) - end - - def find_share - @share = @work_package.members.find(params[:id]) - end - - def find_shares - @shares = load_shares(load_query) - end - - def find_project - @project = @work_package.project - end - - def current_visible_member_count - @current_visible_member_count ||= load_shares(load_query).size - end - - def load_query - @query = ParamsToQueryService.new(Member, - current_user, - query_class: Queries::Members::WorkPackageMemberQuery) - .call(params) - - # Set default filter on the entity - @query.where("entity_id", "=", @work_package.id) - @query.where("entity_type", "=", WorkPackage.name) - @query.where("project_id", "=", @project.id) - - @query.order(name: :asc) unless params[:sortBy] - - @query - end - - def load_shares(query) - query - .results - end -end diff --git a/app/forms/projects/custom_fields/custom_field_mapping_form.rb b/app/forms/projects/custom_fields/custom_field_mapping_form.rb index f91ef2936a3c..3db99fc5b2d8 100644 --- a/app/forms/projects/custom_fields/custom_field_mapping_form.rb +++ b/app/forms/projects/custom_fields/custom_field_mapping_form.rb @@ -28,12 +28,15 @@ module Projects::CustomFields class CustomFieldMappingForm < ApplicationForm + include OpPrimer::ComponentHelpers + form do |form| - form.group(layout: :horizontal) do |group| + form.group(layout: :vertical) do |group| group.project_autocompleter( name: :id, label: Project.model_name.human, visually_hide_label: true, + validation_message: project_ids_error_message, autocomplete_options: { openDirectly: false, focusDirectly: false, @@ -53,16 +56,24 @@ class CustomFieldMappingForm < ApplicationForm end end - def initialize(project_custom_field:) + def initialize(project_mapping:) super() - @project_custom_field = project_custom_field + @project_mapping = project_mapping end private + def project_ids_error_message + @project_mapping + .errors + .messages_for(:project_ids) + .to_sentence + .presence + end + def projects_with_custom_field_mapping ProjectCustomFieldProjectMapping - .where(project_custom_field: @project_custom_field) + .where(custom_field_id: @project_mapping.custom_field_id) .pluck(:project_id) .to_h { |id| [id, id] } end diff --git a/app/forms/queries/projects/form.rb b/app/forms/queries/projects/form.rb index 006e2d96fac9..7e2e2b954faf 100644 --- a/app/forms/queries/projects/form.rb +++ b/app/forms/queries/projects/form.rb @@ -36,7 +36,7 @@ class Queries::Projects::Form < ApplicationForm required: true, autofocus: true, name: "name", - label: Queries::Projects::ProjectQuery.human_attribute_name(:name), + label: ProjectQuery.human_attribute_name(:name), placeholder: I18n.t(:"projects.lists.new.placeholder") ) diff --git a/app/forms/work_packages/share/invitee.rb b/app/forms/shares/invitee.rb similarity index 86% rename from app/forms/work_packages/share/invitee.rb rename to app/forms/shares/invitee.rb index de2d34f73691..e6857c141cd0 100644 --- a/app/forms/work_packages/share/invitee.rb +++ b/app/forms/shares/invitee.rb @@ -25,21 +25,21 @@ # # See COPYRIGHT and LICENSE files for more details. #++ -module WorkPackages::Share +module Shares class Invitee < ApplicationForm form do |user_invite_form| user_invite_form.autocompleter( name: :user_id, - label: I18n.t("work_package.sharing.label_search"), + label: I18n.t("sharing.label_search"), visually_hide_label: true, - data: { "work-packages--share--user-limit-target": "autocompleter" }, + data: { "shares--user-limit-target": "autocompleter" }, autocomplete_options: { component: "opce-user-autocompleter", defaultData: false, - id: "op-share-wp-invite-autocomplete", - placeholder: I18n.t("work_package.sharing.label_search_placeholder"), + id: "op-share-dialog-invite-autocomplete", + placeholder: I18n.t("sharing.label_search_placeholder"), data: { - "test-selector": "op-share-wp-invite-autocomplete" + "test-selector": "op-share-dialog-invite-autocomplete" }, url: ::API::V3::Utilities::PathHelper::ApiV3Path.principals, filters: [{ name: "type", operator: "=", values: %w[User Group] }, diff --git a/app/helpers/attachments_helper.rb b/app/helpers/attachments_helper.rb index 9f3238dd0124..f94ba25e46f5 100644 --- a/app/helpers/attachments_helper.rb +++ b/app/helpers/attachments_helper.rb @@ -45,6 +45,6 @@ def list_attachments(resource, options = {}) options[:inputs] = (options[:inputs] || {}) .reverse_merge(resource:, allowUploading: false, destroyImmediately: true) - angular_component_tag("op-attachments", **options) + angular_component_tag("opce-attachments", **options) end end diff --git a/app/helpers/frontend_asset_helper.rb b/app/helpers/frontend_asset_helper.rb index 9adfd5dbefda..3d1d30b91765 100644 --- a/app/helpers/frontend_asset_helper.rb +++ b/app/helpers/frontend_asset_helper.rb @@ -27,14 +27,14 @@ #++ module FrontendAssetHelper - CLI_DEFAULT_PROXY = 'http://localhost:4200'.freeze + CLI_DEFAULT_PROXY = "http://localhost:4200".freeze def self.assets_proxied? - ENV['OPENPROJECT_DISABLE_DEV_ASSET_PROXY'].blank? && !Rails.env.production? && cli_proxy.present? + ENV["OPENPROJECT_DISABLE_DEV_ASSET_PROXY"].blank? && !Rails.env.production? && cli_proxy.present? end def self.cli_proxy - ENV.fetch('OPENPROJECT_CLI_PROXY', CLI_DEFAULT_PROXY) + ENV.fetch("OPENPROJECT_CLI_PROXY", CLI_DEFAULT_PROXY) end ## diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 25a702eea32b..862942cf2620 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -53,7 +53,7 @@ def short_project_description(project, length = 255) end def projects_columns_options - @projects_columns_options ||= ::Queries::Projects::ProjectQuery + @projects_columns_options ||= ::ProjectQuery .new .available_selects .reject { |c| c.attribute == :hierarchy } diff --git a/app/mailers/sharing_mailer.rb b/app/mailers/sharing_mailer.rb index d01efe5a9169..db6e63fa2978 100644 --- a/app/mailers/sharing_mailer.rb +++ b/app/mailers/sharing_mailer.rb @@ -39,11 +39,11 @@ def optionally_activated_url(back_url, invitation_token) def derive_role_rights(role) case role.builtin when Role::BUILTIN_WORK_PACKAGE_EDITOR - I18n.t("work_package.sharing.permissions.edit") + I18n.t("work_package.permissions.edit") when Role::BUILTIN_WORK_PACKAGE_COMMENTER - I18n.t("work_package.sharing.permissions.comment") + I18n.t("work_package.permissions.comment") when Role::BUILTIN_WORK_PACKAGE_VIEWER - I18n.t("work_package.sharing.permissions.view") + I18n.t("work_package.permissions.view") end end @@ -51,14 +51,14 @@ def derive_allowed_work_package_actions(role) allowed_actions = case role.builtin when Role::BUILTIN_WORK_PACKAGE_EDITOR - [I18n.t("work_package.sharing.permissions.view"), - I18n.t("work_package.sharing.permissions.comment"), - I18n.t("work_package.sharing.permissions.edit")] + [I18n.t("work_package.permissions.view"), + I18n.t("work_package.permissions.comment"), + I18n.t("work_package.permissions.edit")] when Role::BUILTIN_WORK_PACKAGE_COMMENTER - [I18n.t("work_package.sharing.permissions.view"), - I18n.t("work_package.sharing.permissions.comment")] + [I18n.t("work_package.permissions.view"), + I18n.t("work_package.permissions.comment")] when Role::BUILTIN_WORK_PACKAGE_VIEWER - [I18n.t("work_package.sharing.permissions.view")] + [I18n.t("work_package.permissions.view")] end allowed_actions.map(&:downcase) diff --git a/app/helpers/menus/members_helper.rb b/app/menus/members/menu.rb similarity index 66% rename from app/helpers/menus/members_helper.rb rename to app/menus/members/menu.rb index 06fe2d4931d6..795148e108b8 100644 --- a/app/helpers/menus/members_helper.rb +++ b/app/menus/members/menu.rb @@ -1,6 +1,6 @@ #-- copyright # OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH +# Copyright (C) 2010-2024 the OpenProject GmbH # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License version 3. @@ -25,34 +25,30 @@ # # See COPYRIGHT and LICENSE files for more details. #++ +module Members + class Menu < Submenu + attr_reader :project, :params -module Menus - module MembersHelper - def first_level_menu_items - [OpenProject::Menu::MenuGroup.new(header: nil, children: user_status_options)] + nested_menu_items + def initialize(project: nil, params: nil) + super(view_type: nil, project:, params:) end - private - - def user_status_options + def menu_items [ - OpenProject::Menu::MenuItem.new(title: I18n.t("members.menu.all"), - href: project_members_path, - selected: active_filter_count == 0), - OpenProject::Menu::MenuItem.new(title: I18n.t("members.menu.locked"), - href: project_members_path(status: :locked), - selected: selected?(:status, :locked)), - OpenProject::Menu::MenuItem.new(title: I18n.t("members.menu.invited"), - href: project_members_path(status: :invited), - selected: selected?(:status, :invited)) + OpenProject::Menu::MenuGroup.new(header: nil, children: user_status_options), + OpenProject::Menu::MenuGroup.new(header: I18n.t("members.menu.project_roles"), children: project_roles_entries), + OpenProject::Menu::MenuGroup.new(header: I18n.t("members.menu.wp_shares"), children: permission_menu_entries), + OpenProject::Menu::MenuGroup.new(header: I18n.t("members.menu.groups"), children: project_group_entries) ] end - def nested_menu_items + def user_status_options [ - OpenProject::Menu::MenuGroup.new(header: I18n.t("members.menu.project_roles"), children: project_roles_entries), - OpenProject::Menu::MenuGroup.new(header: I18n.t("members.menu.wp_shares"), children: permission_menu_entries), - OpenProject::Menu::MenuGroup.new(header: I18n.t("members.menu.groups"), children: project_group_entries) + OpenProject::Menu::MenuItem.new(title: I18n.t("members.menu.all"), + href: project_members_path(project), + selected: active_filter_count == 0), + menu_item(I18n.t("members.menu.locked"), status: :locked), + menu_item(I18n.t("members.menu.invited"), status: :invited) ] end @@ -61,13 +57,13 @@ def project_roles_entries .where(id: MemberRole.where(member_id: @project.members.select(:id)).select(:role_id)) .distinct .pluck(:id, :name) - .map { |id, name| menu_item(:role_id, id, name) } + .map { |id, name| menu_item(name, role_id: id) } end def permission_menu_entries Members::UserFilterComponent .share_options - .map { |name, id| menu_item(:shared_role_id, id, name) } + .map { |name, id| menu_item(name, shared_role_id: id) } end def project_group_entries @@ -76,19 +72,17 @@ def project_group_entries .order(lastname: :asc) .distinct .pluck(:id, :lastname) - .map { |id, name| menu_item(:group_id, id, name) } + .map { |id, name| menu_item(name, group_id: id) } end - def menu_item(filter_key, id, name) - OpenProject::Menu::MenuItem.new(title: name, - href: project_members_path(filter_key => id), - selected: selected?(filter_key, id)) - end - - def selected?(filter_key, value) + def selected?(query_params) return false if active_filter_count > 1 - params[filter_key] == value.to_s + super + end + + def query_path(query_params) + project_members_path(project, query_params) end def active_filter_count diff --git a/app/helpers/menus/projects.rb b/app/menus/projects/menu.rb similarity index 69% rename from app/helpers/menus/projects.rb rename to app/menus/projects/menu.rb index 76db83891d8a..bf1f277c5d50 100644 --- a/app/helpers/menus/projects.rb +++ b/app/menus/projects/menu.rb @@ -26,33 +26,50 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Menus - class Projects +module Projects + class Menu < Submenu include Rails.application.routes.url_helpers attr_reader :controller_path, :params, :current_user - def initialize(controller_path:, params:, current_user:) - # rubocop:disable Rails/HelperInstanceVariable - @controller_path = controller_path + def initialize(params:, controller_path:, current_user:) @params = params + @controller_path = controller_path @current_user = current_user - # rubocop:enable Rails/HelperInstanceVariable + + super(view_type:, project:, params:) end - def first_level_menu_items + def menu_items [ - OpenProject::Menu::MenuGroup.new(header: nil, - children: main_static_filters), - OpenProject::Menu::MenuGroup.new(header: I18n.t(:"projects.lists.public"), - children: public_filters), - OpenProject::Menu::MenuGroup.new(header: I18n.t(:"projects.lists.my_private"), - children: my_filters), + OpenProject::Menu::MenuGroup.new(header: nil, children: main_static_filters), + OpenProject::Menu::MenuGroup.new(header: I18n.t(:"projects.lists.public"), children: public_filters), + OpenProject::Menu::MenuGroup.new(header: I18n.t(:"projects.lists.my_private"), children: my_filters), OpenProject::Menu::MenuGroup.new(header: I18n.t(:"activerecord.attributes.project.status_code"), children: status_static_filters) ] end + def selected?(query_params) + case controller_path + when "projects" + case params[:query_id] + when nil + query_params[:query_id].to_s == Queries::Projects::Factory::DEFAULT_STATIC + when /\A\d+\z/ + query_params[:query_id].to_s == params[:query_id] + else + query_params[:query_id].to_s == params[:query_id] unless modification_params? + end + when "projects/queries" + query_params[:query_id].to_s == params[:id] + end + end + + def query_path(query_params) + projects_path(query_params) + end + private def main_static_filters @@ -74,44 +91,22 @@ def status_static_filters def static_filters(ids) ids.map do |id| - query_menu_item(::Queries::Projects::Factory.static_query(id), id:) + menu_item(::Queries::Projects::Factory.static_query(id).name, query_id: id) end end def public_filters - ::Queries::Projects::ProjectQuery + ::ProjectQuery .public_lists .order(:name) - .map { |query| query_menu_item(query) } + .map { |query| menu_item(query.name, query_id: query.id) } end def my_filters - ::Queries::Projects::ProjectQuery + ::ProjectQuery .private_lists(user: current_user) .order(:name) - .map { |query| query_menu_item(query) } - end - - def query_menu_item(query, id: nil) - OpenProject::Menu::MenuItem.new(title: query.name, - href: projects_path(query_id: id || query.id), - selected: query_item_selected?(id || query.id)) - end - - def query_item_selected?(id) - case controller_path - when "projects" - case params[:query_id] - when nil - id.to_s == Queries::Projects::Factory::DEFAULT_STATIC - when /\A\d+\z/ - id.to_s == params[:query_id] - else - id.to_s == params[:query_id] unless modification_params? - end - when "projects/queries" - id.to_s == params[:id] - end + .map { |query| menu_item(query.name, query_id: query.id) } end def modification_params? diff --git a/app/menus/submenu.rb b/app/menus/submenu.rb index e2866c67922f..0e495d4672d1 100644 --- a/app/menus/submenu.rb +++ b/app/menus/submenu.rb @@ -48,7 +48,7 @@ def starred_queries base_query .where("starred" => "t") .pluck(:id, :name) - .map { |id, name| menu_item(query_params(id), name) } + .map { |id, name| menu_item(name, query_params(id)) } end def default_queries @@ -60,7 +60,7 @@ def global_queries .where("starred" => "f") .where("public" => "t") .pluck(:id, :name) - .map { |id, name| menu_item(query_params(id), name) } + .map { |id, name| menu_item(name, query_params(id)) } end def custom_queries @@ -68,7 +68,7 @@ def custom_queries .where("starred" => "f") .where("public" => "f") .pluck(:id, :name) - .map { |id, name| menu_item(query_params(id), name) } + .map { |id, name| menu_item(name, query_params(id)) } end def base_query @@ -89,7 +89,7 @@ def query_params(id) { query_id: id } end - def menu_item(query_params, name) + def menu_item(name, query_params) OpenProject::Menu::MenuItem.new(title: name, href: query_path(query_params), selected: selected?(query_params)) @@ -102,6 +102,10 @@ def selected?(query_params) end end + if query_params.empty? && params[:filters].present? + return false + end + true end diff --git a/app/models/member.rb b/app/models/member.rb index dd918e6d96e5..0419e68cb3f4 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -31,7 +31,7 @@ class Member < ApplicationRecord ALLOWED_ENTITIES = [ "WorkPackage", - "Queries::Projects::ProjectQuery" + "ProjectQuery" ].freeze extend DeprecatedAlias @@ -56,6 +56,7 @@ class Member < ApplicationRecord :of_any_project, :of_work_package, :of_any_work_package, + :of_entity, :of_any_entity, :of_anything_in_project, :visible, diff --git a/app/models/members/scopes/of_entity.rb b/app/models/members/scopes/of_entity.rb new file mode 100644 index 000000000000..b9178a060458 --- /dev/null +++ b/app/models/members/scopes/of_entity.rb @@ -0,0 +1,41 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-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 Members::Scopes + module OfEntity + extend ActiveSupport::Concern + + class_methods do + # Find all members of a specific Work Package + def of_entity(entity) + of_any_entity + .where(entity:) + end + end + end +end diff --git a/app/models/news.rb b/app/models/news.rb index 202723aee466..eced61e2b18d 100644 --- a/app/models/news.rb +++ b/app/models/news.rb @@ -33,6 +33,7 @@ class News < ApplicationRecord order(:created_at) }, as: :commented, dependent: :delete_all + validates :project, presence: true validates :title, presence: true validates :title, length: { maximum: 256 } validates :summary, length: { maximum: 255 } @@ -46,6 +47,11 @@ class News < ApplicationRecord references: :projects, date_column: "#{table_name}.created_at" + acts_as_attachable view_permission: :view_news, + add_on_new_permission: :manage_news, + add_on_persisted_permission: :manage_news, + delete_permission: :manage_news + acts_as_watchable after_create :add_author_as_watcher diff --git a/app/models/project.rb b/app/models/project.rb index cd7c1a4cf46a..1bb82198272f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -40,7 +40,6 @@ class Project < ApplicationRecord include ::Scopes::Scoped - # Maximum length for project identifiers IDENTIFIER_MAX_LENGTH = 100 @@ -252,6 +251,13 @@ def enabled_module_names enabled_modules.map(&:name) end + def reload(*) + @allowed_permissions = nil + @allowed_actions = nil + + super + end + def allowed_permissions @allowed_permissions ||= begin diff --git a/app/models/queries/projects/project_queries/scopes/allowed_to.rb b/app/models/project_queries/scopes/allowed_to.rb similarity index 98% rename from app/models/queries/projects/project_queries/scopes/allowed_to.rb rename to app/models/project_queries/scopes/allowed_to.rb index 01b2dea5ca10..d1686cc37008 100644 --- a/app/models/queries/projects/project_queries/scopes/allowed_to.rb +++ b/app/models/project_queries/scopes/allowed_to.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -module Queries::Projects::ProjectQueries::Scopes +module ProjectQueries::Scopes module AllowedTo extend ActiveSupport::Concern diff --git a/app/models/queries/projects/project_query.rb b/app/models/project_query.rb similarity index 97% rename from app/models/queries/projects/project_query.rb rename to app/models/project_query.rb index 9a1d7736d320..bf5aaaca35cc 100644 --- a/app/models/queries/projects/project_query.rb +++ b/app/models/project_query.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Queries::Projects::ProjectQuery < ApplicationRecord +class ProjectQuery < ApplicationRecord include Queries::BaseQuery include Queries::Serialization::Hash include HasMembers diff --git a/app/models/queries/members.rb b/app/models/queries/members.rb index 8f55951d4dc3..98228d61265e 100644 --- a/app/models/queries/members.rb +++ b/app/models/queries/members.rb @@ -50,7 +50,7 @@ module Queries::Members order Orders::StatusOrder end - ::Queries::Register.register(WorkPackageMemberQuery) do + ::Queries::Register.register(EntityMemberQuery) do filter Filters::NameFilter filter Filters::AnyNameAttributeFilter filter Filters::ProjectFilter diff --git a/app/models/queries/members/work_package_member_query.rb b/app/models/queries/members/entity_member_query.rb similarity index 94% rename from app/models/queries/members/work_package_member_query.rb rename to app/models/queries/members/entity_member_query.rb index 36436a38a6f3..27714076f0e1 100644 --- a/app/models/queries/members/work_package_member_query.rb +++ b/app/models/queries/members/entity_member_query.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -class Queries::Members::WorkPackageMemberQuery < Queries::Members::MemberQuery +class Queries::Members::EntityMemberQuery < Queries::Members::MemberQuery def default_scope Member.joins(:member_roles).merge(MemberRole.only_non_inherited) end diff --git a/app/models/queries/projects/factory.rb b/app/models/queries/projects/factory.rb index bbc7dc19acaa..b4ead080663a 100644 --- a/app/models/queries/projects/factory.rb +++ b/app/models/queries/projects/factory.rb @@ -107,7 +107,7 @@ def static_query_status_at_risk private def list_with(name) - Queries::Projects::ProjectQuery.new(name: I18n.t(name)) do |query| + ProjectQuery.new(name: I18n.t(name)) do |query| query.order("lft" => "asc") query.select(*Setting.enabled_projects_columns, add_not_existing: false) @@ -133,7 +133,7 @@ def find_static_query_and_set_attributes(id, params, user, duplicate:) end def find_persisted_query_and_set_attributes(id, params, user, duplicate:) - query = Queries::Projects::ProjectQuery.visible(user).find_by(id:) + query = ProjectQuery.visible(user).find_by(id:) return unless query @@ -150,7 +150,7 @@ def find_persisted_query_and_set_attributes(id, params, user, duplicate:) end def duplicate_query(query) - Queries::Projects::ProjectQuery.new(query.attributes.slice("filters", "orders", "selects")) + ProjectQuery.new(query.attributes.slice("filters", "orders", "selects")) end def set_query_attributes(query, params, user) diff --git a/app/models/setting.rb b/app/models/setting.rb index 749ce4dfbf44..0d885dd16a17 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -133,7 +133,7 @@ def respond_to_missing?(method_name, include_private = false) private def accessor_base_name(name) - name.to_s.sub(/(_writable\?)|(\?)|=\z/, '') + name.to_s.sub(/(_writable\?)|(\?)|=\z/, "") end end @@ -337,7 +337,7 @@ def self.deserialize(name, value) if definition.serialized? && value.is_a?(String) deserialize_hash(value) - elsif value != ''.freeze && !value.nil? + elsif value != "".freeze && !value.nil? read_formatted_setting(value, definition.format) else definition.format == :string ? value : nil diff --git a/app/models/user.rb b/app/models/user.rb index ca6f7e0e1924..dd3c1aaa3c3d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,11 +26,11 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require 'digest/sha1' +require "digest/sha1" class User < Principal VALID_NAME_REGEX = /\A[\d\p{Alpha}\p{Mark}\p{Space}\p{Emoji}'’´\-_.,@()+&*–]+\z/ - CURRENT_USER_LOGIN_ALIAS = 'me'.freeze + CURRENT_USER_LOGIN_ALIAS = "me".freeze USER_FORMATS_STRUCTURE = { firstname_lastname: %i[firstname lastname], firstname: [:firstname], @@ -45,40 +45,40 @@ class User < Principal include ::Users::PermissionChecks extend DeprecatedAlias - has_many :watches, class_name: 'Watcher', + has_many :watches, class_name: "Watcher", dependent: :delete_all has_many :changesets, dependent: :nullify has_many :passwords, -> { - order('id DESC') - }, class_name: 'UserPassword', + order("id DESC") + }, class_name: "UserPassword", dependent: :destroy, inverse_of: :user - has_one :rss_token, class_name: '::Token::RSS', dependent: :destroy - has_one :api_token, class_name: '::Token::API', dependent: :destroy + has_one :rss_token, class_name: "::Token::RSS", dependent: :destroy + has_one :api_token, class_name: "::Token::API", dependent: :destroy # The user might have one invitation token - has_one :invitation_token, class_name: '::Token::Invitation', dependent: :destroy + has_one :invitation_token, class_name: "::Token::Invitation", dependent: :destroy # everytime a user subscribes to a calendar, a new ical_token is generated # unlike on other token types, all previously generated ical_tokens are kept # in order to keep all previously generated ical urls valid and usable - has_many :ical_tokens, class_name: '::Token::ICal', dependent: :destroy + has_many :ical_tokens, class_name: "::Token::ICal", dependent: :destroy belongs_to :ldap_auth_source, optional: true # Authorized OAuth grants has_many :oauth_grants, - class_name: 'Doorkeeper::AccessGrant', - foreign_key: 'resource_owner_id' + class_name: "Doorkeeper::AccessGrant", + foreign_key: "resource_owner_id" # User-defined oauth applications has_many :oauth_applications, - class_name: 'Doorkeeper::Application', + class_name: "Doorkeeper::Application", as: :owner # Meeting memberships has_many :meeting_participants, - class_name: 'MeetingParticipant', + class_name: "MeetingParticipant", inverse_of: :user, dependent: :destroy @@ -86,7 +86,7 @@ class User < Principal dependent: :destroy has_many :project_queries, - class_name: 'Queries::Projects::ProjectQuery', + class_name: "ProjectQuery", inverse_of: :user, dependent: :destroy @@ -109,7 +109,7 @@ def self.create_blocked_scope(scope, blocked) def self.blocked_condition(blocked) block_duration = Setting.brute_force_block_minutes.to_i.minutes blocked_if_login_since = Time.now - block_duration - negation = blocked ? '' : 'NOT' + negation = blocked ? "" : "NOT" ["#{negation} (users.failed_login_count >= ? AND users.last_failed_login_on > ?)", Setting.brute_force_block_after_failed_logins.to_i, @@ -141,7 +141,7 @@ def self.blocked_condition(blocked) validates :password, confirmation: { allow_nil: true, - message: ->(*) { I18n.t('activerecord.errors.models.user.attributes.password_confirmation.confirmation') } + message: ->(*) { I18n.t("activerecord.errors.models.user.attributes.password_confirmation.confirmation") } } auto_strip_attributes :login, nullify: false @@ -210,7 +210,7 @@ def self.try_to_login(login, password, session = nil) # Tries to authenticate a user in the database via external auth source # or password stored in the database - def self.try_authentication_for_existing_user(user, password, session = nil) + def self.try_authentication_for_existing_user(user, password, session = nil) # rubocop:disable Metrics/PerceivedComplexity activate_user! user, session if session return nil if !user.active? || OpenProject::Configuration.disable_password_login? @@ -255,7 +255,7 @@ def self.try_authentication_and_create_user(login, password) # Returns the user who matches the given autologin +key+ or nil def self.try_to_autologin(key) - token = Token::AutoLogin.find_by_plaintext_value(key) + token = Token::AutoLogin.find_by_plaintext_value(key) # rubocop:disable Rails/DynamicFindBy # Make sure there's only 1 token that matches the key if token && ((token.created_at > Setting.autologin.to_i.day.ago) && token.user && token.user.active?) token.user @@ -295,7 +295,7 @@ def name(formatter = nil) def authentication_provider return if identity_url.blank? - identity_url.split(':', 2).first.titleize + identity_url.split(":", 2).first.titleize end ## @@ -525,20 +525,20 @@ def missing_authentication_method? # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only # one anonymous user per database. - def self.anonymous + def self.anonymous # rubocop:disable Metrics/AbcSize RequestStore[:anonymous_user] ||= begin anonymous_user = AnonymousUser.first if anonymous_user.nil? (anonymous_user = AnonymousUser.new.tap do |u| - u.lastname = 'Anonymous' - u.login = '' - u.firstname = '' - u.mail = '' + u.lastname = "Anonymous" + u.login = "" + u.firstname = "" + u.mail = "" u.status = User.statuses[:active] end).save - raise 'Unable to create the anonymous user.' if anonymous_user.new_record? + raise "Unable to create the anonymous user." if anonymous_user.new_record? end anonymous_user end @@ -560,7 +560,7 @@ def self.system system_user.save(validate: false) - raise 'Unable to create the automatic migration user.' unless system_user.persisted? + raise "Unable to create the automatic migration user." unless system_user.persisted? end system_user @@ -586,7 +586,7 @@ def password_meets_requirements if former_passwords_include?(password) errors.add(:password, - I18n.t('activerecord.errors.models.user.attributes.password.reused', + I18n.t("activerecord.errors.models.user.attributes.password.reused", count: Setting[:password_count_former_banned].to_i)) end end @@ -596,7 +596,7 @@ def password_meets_requirements def self.mail_regexp(mail) separators = Regexp.escape(Setting.mail_suffix_separators) - recipient, domain = mail.split('@').map { |part| Regexp.escape(part) } + recipient, domain = mail.split("@").map { |part| Regexp.escape(part) } skip_suffix_check = recipient.nil? || Setting.mail_suffix_separators.empty? || recipient.match?(/.+[#{separators}].+/) regexp = "^#{recipient}([#{separators}][^@]+)*@#{domain}$" @@ -675,6 +675,6 @@ def log_failed_login_timestamp end def self.default_admin_account_changed? - !User.active.find_by_login('admin').try(:current_password).try(:matches_plaintext?, 'admin') + !User.active.find_by_login("admin").try(:current_password).try(:matches_plaintext?, "admin") # rubocop:disable Rails/DynamicFindBy end end diff --git a/app/models/users/permission_checks.rb b/app/models/users/permission_checks.rb index 9fe321cb7d78..3c60d3377de8 100644 --- a/app/models/users/permission_checks.rb +++ b/app/models/users/permission_checks.rb @@ -106,14 +106,24 @@ def member_of?(project) roles_for_project(project).any?(&:member?) end + # Returns all permissions the user may have for a given context. + # "May" because this method does not check e.g. whether the module + # the permission belongs to is active. def all_permissions_for(context) - Authorization - .roles(self, context) - .includes(:role_permissions) - .pluck(:permission) - .compact - .map(&:to_sym) - .uniq + if admin? + OpenProject::AccessControl + .permissions + .select { |p| p.permissible_on?(context) && p.grant_to_admin? } + .map(&:name) + else + Authorization + .roles(self, context) + .includes(:role_permissions) + .pluck(:permission) + .compact + .map(&:to_sym) + .uniq + end end # Helper method to be used in places where we just throw anything into the permission check and don't know what diff --git a/app/models/work_package/exports/macros/attributes.rb b/app/models/work_package/exports/macros/attributes.rb index 8b50adfa503e..d8b30f793406 100644 --- a/app/models/work_package/exports/macros/attributes.rb +++ b/app/models/work_package/exports/macros/attributes.rb @@ -77,16 +77,16 @@ def self.resolve_match(type, model_s, id, attribute, work_package, user) elsif model_s == "project" resolve_project_match(id || work_package.project.id, type, attribute, user) else - msg_macro_error I18n.t('export.macro.model_not_found', model: model_s) + msg_macro_error I18n.t("export.macro.model_not_found", model: model_s) end end def self.msg_macro_error(message) - msg_inline I18n.t('export.macro.error', message:) + msg_inline I18n.t("export.macro.error", message:) end def self.msg_macro_error_rich_text - msg_inline I18n.t('export.macro.rich_text_unsupported') + msg_inline I18n.t("export.macro.rich_text_unsupported") end def self.msg_inline(message) @@ -109,11 +109,11 @@ def self.resolve_label(model, attribute) def self.resolve_work_package_match(id, type, attribute, user) return resolve_label_work_package(attribute) if type == "label" - return msg_macro_error(I18n.t('export.macro.model_not_found', model: type)) unless type == "value" + return msg_macro_error(I18n.t("export.macro.model_not_found", model: type)) unless type == "value" work_package = WorkPackage.visible(user).find_by(id:) if work_package.nil? - return msg_macro_error(I18n.t('export.macro.resource_not_found', resource: "#{WorkPackage.name} #{id}")) + return msg_macro_error(I18n.t("export.macro.resource_not_found", resource: "#{WorkPackage.name} #{id}")) end resolve_value_work_package(work_package, attribute) diff --git a/app/models/work_package/pdf_export/gantt/gantt_builder_weeks.rb b/app/models/work_package/pdf_export/gantt/gantt_builder_weeks.rb index 9fe3bbf88bca..54ee2e633820 100644 --- a/app/models/work_package/pdf_export/gantt/gantt_builder_weeks.rb +++ b/app/models/work_package/pdf_export/gantt/gantt_builder_weeks.rb @@ -30,7 +30,7 @@ module WorkPackage::PDFExport::Gantt class GanttBuilderWeeks < GanttBuilder def build_column_dates_range(range) range - .map { |d| [d.year, d.cweek] } + .map { |d| [d.cwyear, d.cweek] } .uniq .map { |year, week| Date.commercial(year, week, 1) } end diff --git a/app/models/work_package/pdf_export/markdown.rb b/app/models/work_package/pdf_export/markdown.rb index 84cd5b1c54ad..03bdde5f7461 100644 --- a/app/models/work_package/pdf_export/markdown.rb +++ b/app/models/work_package/pdf_export/markdown.rb @@ -33,9 +33,10 @@ class MD2PDF include MarkdownToPDF::Core include MarkdownToPDF::Parser - def initialize(styling_yml) + def initialize(styling_yml, pdf) @styles = MarkdownToPDF::Styles.new(styling_yml) init_options({ auto_generate_header_ids: false }) + pdf_init_md2pdf_fonts(pdf) # @hyphens = Hyphen.new('en', false) end @@ -96,7 +97,7 @@ def warn(text, element, node) end def write_markdown!(work_package, markdown) - md2pdf = MD2PDF.new(styles.wp_markdown_styling_yml) + md2pdf = MD2PDF.new(styles.wp_markdown_styling_yml, pdf) md2pdf.draw_markdown(markdown, pdf, ->(src) { with_images? ? attachment_image_filepath(work_package, src) : nil }) diff --git a/app/models/work_package/pdf_export/schema.json b/app/models/work_package/pdf_export/schema.json index 3546fef553d9..50c64700b44a 100644 --- a/app/models/work_package/pdf_export/schema.json +++ b/app/models/work_package/pdf_export/schema.json @@ -392,7 +392,7 @@ "group_heading" : { "type" : "object", "title" : "Overview group heading", - "description" : "Styling for the group lavel if grouping is activated", + "description" : "Styling for the group label if grouping is activated", "x-example" : { "group_heading" : { "size" : 11, @@ -981,6 +981,9 @@ "task_list_point" : { "title" : "Markdown task list point", "$ref" : "#/$defs/task_list_point" + }, + "alerts": { + "$ref": "#/$defs/alerts" } }, "patternProperties" : { @@ -1744,6 +1747,65 @@ "type" : "boolean" } } + }, + "alert": { + "type": "object", + "title": "Alert", + "description": "Styling to denote a quote as alert box", + "x-example": { + "ALERT": { + "alert_color": "f4f9ff", + "border_color": "f4f9ff", + "border_width": 2, + "no_border_right": true, + "no_border_left": false, + "no_border_bottom": true, + "no_border_top": true + } + }, + "properties": { + "background_color": { + "$ref": "#/$defs/color" + }, + "alert_color": { + "$ref": "#/$defs/color" + } + }, + "allOf": [ + { + "$ref": "#/$defs/font" + }, + { + "$ref": "#/$defs/border" + }, + { + "$ref": "#/$defs/padding" + }, + { + "$ref": "#/$defs/margin" + } + ] + }, + "alerts": { + "type": "object", + "title": "alert boxes (styled blockquotes)", + "properties": { + "NOTE": { + "$ref": "#/$defs/alert" + }, + "TIP": { + "$ref": "#/$defs/alert" + }, + "WARNING": { + "$ref": "#/$defs/alert" + }, + "IMPORTANT": { + "$ref": "#/$defs/alert" + }, + "CAUTION": { + "$ref": "#/$defs/alert" + } + } } } } diff --git a/app/models/work_package/pdf_export/standard.yml b/app/models/work_package/pdf_export/standard.yml index 9a514dbceba4..915d6dd37c2e 100644 --- a/app/models/work_package/pdf_export/standard.yml +++ b/app/models/work_package/pdf_export/standard.yml @@ -192,6 +192,62 @@ work_package: size: 8 border_width: 0.25 padding: 5 + alerts: + NOTE: + border_color: '0969da' + alert_color: '0969da' + padding: '4mm' + size: 10 + styles: [ ] + border_width: 2 + no_border_right: true + no_border_left: false + no_border_bottom: true + no_border_top: true + TIP: + border_color: '1a7f37' + alert_color: '1a7f37' + padding: '4mm' + size: 10 + styles: [ ] + border_width: 2 + no_border_right: true + no_border_left: false + no_border_bottom: true + no_border_top: true + IMPORTANT: + border_color: '8250df' + alert_color: '8250df' + padding: '4mm' + size: 10 + styles: [ ] + border_width: 2 + no_border_right: true + no_border_left: false + no_border_bottom: true + no_border_top: true + WARNING: + border_color: 'bf8700' + alert_color: 'bf8700' + padding: '4mm' + size: 10 + styles: [ ] + border_width: 2 + no_border_right: true + no_border_left: false + no_border_bottom: true + no_border_top: true + CAUTION: + border_color: 'd1242f' + alert_color: 'd1242f' + size: 10 + styles: [ ] + padding: '4mm' + border_width: 2 + no_border_right: true + no_border_left: false + no_border_bottom: true + no_border_top: true cover: header: diff --git a/app/services/authorization/user_permissible_service.rb b/app/services/authorization/user_permissible_service.rb index 133a16a26523..a3c8e2629b14 100644 --- a/app/services/authorization/user_permissible_service.rb +++ b/app/services/authorization/user_permissible_service.rb @@ -9,7 +9,6 @@ def initialize(user) def allowed_globally?(permission) perms = contextual_permissions(permission, :global) return false unless authorizable_user? - return true if admin_and_all_granted_to_admin?(perms) cached_permissions(nil).intersect?(perms.map(&:name)) end @@ -19,9 +18,7 @@ def allowed_in_project?(permission, projects_to_check) return false if projects_to_check.blank? return false unless authorizable_user? - projects = Array(projects_to_check) - - projects.all? do |project| + Array(projects_to_check).all? do |project| allowed_in_single_project?(perms, project) end end @@ -29,7 +26,6 @@ def allowed_in_project?(permission, projects_to_check) def allowed_in_any_project?(permission) perms = contextual_permissions(permission, :project) return false unless authorizable_user? - return true if admin_and_all_granted_to_admin?(perms) cached_in_any_project?(perms) end @@ -51,7 +47,6 @@ def allowed_in_any_entity?(permission, entity_class, in_project: nil) perms = contextual_permissions(permission, context_name(entity_class)) return false unless authorizable_user? return false if in_project && !(in_project.active? || in_project.being_archived?) - return true if admin_and_all_granted_to_admin?(perms) if entity_is_project_scoped?(entity_class) allowed_in_any_project_scoped_entity?(perms, entity_class, in_project:) @@ -85,7 +80,6 @@ def allowed_in_single_project?(permissions, project) permissions_filtered_for_project = permissions_by_enabled_project_modules(project, permissions) return false if permissions_filtered_for_project.empty? - return true if admin_and_all_granted_to_admin?(permissions) cached_permissions(project).intersect?(permissions_filtered_for_project) end @@ -106,7 +100,6 @@ def allowed_in_single_project_scoped_entity?(permissions, entity) permissions_filtered_for_project = permissions_by_enabled_project_modules(entity.project, permissions) return false if permissions_filtered_for_project.empty? - return true if admin_and_all_granted_to_admin?(permissions) # The combination of this is better then doing # EntityClass.allowed_to(user, permission).exists?. diff --git a/app/services/members/delete_by_principal_service.rb b/app/services/members/delete_by_principal_service.rb index 620ce72035df..f52fd5c89999 100644 --- a/app/services/members/delete_by_principal_service.rb +++ b/app/services/members/delete_by_principal_service.rb @@ -61,21 +61,15 @@ def call(params) def delete_project_member project_member = Member.of_project(project).find_by!(principal:) - Members::DeleteService - .new(user:, model: project_member) - .call + Members::DeleteService.new(user:, model: project_member).call end def delete_work_package_share(model) - WorkPackageMembers::DeleteService - .new(user:, model:) - .call + Shares::DeleteService.new(user:, model:, contract_class: Shares::WorkPackages::DeleteContract).call end def delete_work_package_share_with_role_id(model, role_id) - WorkPackageMembers::DeleteRoleService - .new(user:, model:) - .call(role_id:) + Shares::DeleteRoleService.new(user:, model:, contract_class: Shares::WorkPackages::DeleteContract).call(role_id:) end def work_package_shares_scope diff --git a/app/services/news/create_service.rb b/app/services/news/create_service.rb new file mode 100644 index 000000000000..97af280e9af4 --- /dev/null +++ b/app/services/news/create_service.rb @@ -0,0 +1,31 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +class News::CreateService < BaseServices::Create + include Attachments::ReplaceAttachments +end diff --git a/app/services/news/delete_service.rb b/app/services/news/delete_service.rb new file mode 100644 index 000000000000..04d7b5381c83 --- /dev/null +++ b/app/services/news/delete_service.rb @@ -0,0 +1,30 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +class News::DeleteService < BaseServices::Delete +end diff --git a/app/services/news/set_attributes_service.rb b/app/services/news/set_attributes_service.rb new file mode 100644 index 000000000000..b6274c28460c --- /dev/null +++ b/app/services/news/set_attributes_service.rb @@ -0,0 +1,43 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +class News::SetAttributesService < BaseServices::SetAttributes + include Attachments::SetReplacements + + private + + def set_default_attributes(*) + set_default_author + end + + def set_default_author + model.change_by_system do + model.author = user + end + end +end diff --git a/app/services/news/update_service.rb b/app/services/news/update_service.rb new file mode 100644 index 000000000000..d0e1ef90f959 --- /dev/null +++ b/app/services/news/update_service.rb @@ -0,0 +1,31 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +class News::UpdateService < BaseServices::Update + include Attachments::ReplaceAttachments +end diff --git a/app/services/params_to_query_service.rb b/app/services/params_to_query_service.rb index 11d93f2e6451..f117459b7a6f 100644 --- a/app/services/params_to_query_service.rb +++ b/app/services/params_to_query_service.rb @@ -123,7 +123,9 @@ def set_query_class(query_class, model) else model_name = model.name - "::Queries::#{model_name.pluralize}::#{model_name.demodulize}Query".constantize + # Some queries exist as Queries::Models::ModelQuery others as ModelQuery + "::Queries::#{model_name.pluralize}::#{model_name.demodulize}Query".safe_constantize || + "::#{model_name.demodulize}Query".constantize end end end diff --git a/app/services/projects/copy_service.rb b/app/services/projects/copy_service.rb index 9983adeb566a..72c3990f3a98 100644 --- a/app/services/projects/copy_service.rb +++ b/app/services/projects/copy_service.rb @@ -99,8 +99,9 @@ def after_perform(call) end def copy_activated_custom_fields(call) - call.result.project_custom_field_ids = source.project_custom_field_ids + call.result.project_custom_field_ids = source.project_custom_field_ids end + def contract_options { copy_source: source, validate_model: true } end diff --git a/app/services/queries/projects/project_queries/create_service.rb b/app/services/queries/projects/project_queries/create_service.rb index 0aa6deea366c..7d8482ec9698 100644 --- a/app/services/queries/projects/project_queries/create_service.rb +++ b/app/services/queries/projects/project_queries/create_service.rb @@ -35,4 +35,8 @@ def initialize(from: nil, **) def instance(_params) @from || super end + + def instance_class + ProjectQuery + end end diff --git a/app/services/work_package_members/concerns/role_assignment.rb b/app/services/shares/concerns/role_assignment.rb similarity index 90% rename from app/services/work_package_members/concerns/role_assignment.rb rename to app/services/shares/concerns/role_assignment.rb index 31bdb1194332..0df795fdd0c3 100644 --- a/app/services/work_package_members/concerns/role_assignment.rb +++ b/app/services/shares/concerns/role_assignment.rb @@ -28,11 +28,11 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -module WorkPackageMembers::Concerns::RoleAssignment +module Shares::Concerns::RoleAssignment include Members::Concerns::RoleAssignment - # Work package memberships have a unique distinction from - # project memberships. A User should be able to be granted + # Memberships via shares have a unique distinction from + # regular project memberships. A User should be able to be granted # "Role X" independently and via a group. Meaning that for role assignment # as compared to Project memberships, the existing roles we want to take # into account are those that have not been inherited. diff --git a/app/services/work_package_members/create_or_update_service.rb b/app/services/shares/create_or_update_service.rb similarity index 70% rename from app/services/work_package_members/create_or_update_service.rb rename to app/services/shares/create_or_update_service.rb index 302f55b36a39..becdd8681719 100644 --- a/app/services/work_package_members/create_or_update_service.rb +++ b/app/services/shares/create_or_update_service.rb @@ -26,29 +26,27 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -class WorkPackageMembers::CreateOrUpdateService - def initialize(user:, contract_class: nil, contract_options: {}) +class Shares::CreateOrUpdateService + def initialize(user:, create_contract_class:, update_contract_class:, contract_options: {}) self.user = user - self.contract_class = contract_class + self.create_contract_class = create_contract_class + self.update_contract_class = update_contract_class self.contract_options = contract_options end def call(entity:, user_id:, **) - actual_service(entity, user_id) - .call(entity:, user_id:, **) + actual_service(entity, user_id).call(entity:, user_id:, **) end private - attr_accessor :user, :contract_class, :contract_options + attr_accessor :user, :create_contract_class, :update_contract_class, :contract_options def actual_service(entity, user_id) if (member = Member.find_by(entity:, principal: user_id)) - WorkPackageMembers::UpdateService - .new(user:, model: member, contract_class:, contract_options:) + Shares::UpdateService.new(user:, model: member, contract_class: update_contract_class, contract_options:) else - WorkPackageMembers::CreateService - .new(user:, contract_class:, contract_options:) + Shares::CreateService.new(user:, contract_class: create_contract_class, contract_options:) end end end diff --git a/app/services/work_package_members/create_service.rb b/app/services/shares/create_service.rb similarity index 71% rename from app/services/work_package_members/create_service.rb rename to app/services/shares/create_service.rb index ca0706235846..4f6b11f5e889 100644 --- a/app/services/work_package_members/create_service.rb +++ b/app/services/shares/create_service.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -class WorkPackageMembers::CreateService < BaseServices::Create +class Shares::CreateService < BaseServices::Create private def instance_class @@ -36,29 +36,28 @@ def instance_class def after_perform(service_call) return service_call unless service_call.success? - work_package_member = service_call.result + share = service_call.result - add_group_memberships(work_package_member) - send_notification(work_package_member) + add_group_memberships(share) + send_notification(share) service_call end - def add_group_memberships(work_package_member) - return unless work_package_member.principal.is_a?(Group) + def add_group_memberships(share) + return unless share.principal.is_a?(Group) Groups::CreateInheritedRolesService - .new(work_package_member.principal, - current_user: user, - contract_class: EmptyContract) - .call(user_ids: work_package_member.principal.user_ids, + .new(share.principal, current_user: user, contract_class: EmptyContract) + .call(user_ids: share.principal.user_ids, send_notifications: false, - project_ids: [work_package_member.project_id]) + project_ids: [share.project_id]) # TODO: Here we should add project_id and the entity id as well end - def send_notification(work_package_member) + def send_notification(share) + # TODO: We should select what sort of notification is sent out based on the shared entity OpenProject::Notifications.send(OpenProject::Events::WORK_PACKAGE_SHARED, - work_package_member:, + work_package_member: share, send_notifications: true) end end diff --git a/app/services/work_package_members/delete_role_service.rb b/app/services/shares/delete_role_service.rb similarity index 94% rename from app/services/work_package_members/delete_role_service.rb rename to app/services/shares/delete_role_service.rb index 54a3710a501d..edde8919863c 100644 --- a/app/services/work_package_members/delete_role_service.rb +++ b/app/services/shares/delete_role_service.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -class WorkPackageMembers::DeleteRoleService < WorkPackageMembers::DeleteService +class Shares::DeleteRoleService < Shares::DeleteService def destroy(object) if object.member_roles.where.not("inherited_from IS NULL AND role_id = ?", params[:role_id]).empty? super diff --git a/app/services/work_package_members/delete_service.rb b/app/services/shares/delete_service.rb similarity index 82% rename from app/services/work_package_members/delete_service.rb rename to app/services/shares/delete_service.rb index 1574b02a09a6..b935e618a04d 100644 --- a/app/services/work_package_members/delete_service.rb +++ b/app/services/shares/delete_service.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -class WorkPackageMembers::DeleteService < BaseServices::Delete +class Shares::DeleteService < BaseServices::Delete include Members::Concerns::CleanedUp def destroy(object) @@ -41,17 +41,17 @@ def destroy(object) def after_perform(service_call) super.tap do |call| - work_package_member = call.result + share = call.result - cleanup_for_group(work_package_member) + cleanup_for_group(share) end end - def cleanup_for_group(work_package_member) - return unless work_package_member.principal.is_a?(Group) + def cleanup_for_group(share) + return unless share.principal.is_a?(Group) Groups::CleanupInheritedRolesService - .new(work_package_member.principal, current_user: user, contract_class: EmptyContract) + .new(share.principal, current_user: user, contract_class: EmptyContract) .call end end diff --git a/app/services/work_package_members/set_attributes_service.rb b/app/services/shares/set_attributes_service.rb similarity index 89% rename from app/services/work_package_members/set_attributes_service.rb rename to app/services/shares/set_attributes_service.rb index 70bc10ec483d..1ef7f078e45f 100644 --- a/app/services/work_package_members/set_attributes_service.rb +++ b/app/services/shares/set_attributes_service.rb @@ -26,9 +26,9 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -module WorkPackageMembers +module Shares class SetAttributesService < ::BaseServices::SetAttributes - prepend WorkPackageMembers::Concerns::RoleAssignment + prepend Shares::Concerns::RoleAssignment private @@ -36,7 +36,9 @@ def set_attributes(params) super model.change_by_system do - model.project = model.entity&.project + if model.entity.respond_to?(:project) + model.project = model.entity&.project + end end end end diff --git a/app/services/work_package_members/update_service.rb b/app/services/shares/update_service.rb similarity index 75% rename from app/services/work_package_members/update_service.rb rename to app/services/shares/update_service.rb index 42dae489b44d..a065256d24d5 100644 --- a/app/services/work_package_members/update_service.rb +++ b/app/services/shares/update_service.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -class WorkPackageMembers::UpdateService < BaseServices::Update +class Shares::UpdateService < BaseServices::Update include Members::Concerns::CleanedUp protected @@ -34,20 +34,16 @@ class WorkPackageMembers::UpdateService < BaseServices::Update def after_perform(service_call) return service_call unless service_call.success? - work_package_member = service_call.result + share = service_call.result - update_group_roles(work_package_member) if work_package_member.principal.is_a?(Group) + update_group_roles(share) if share.principal.is_a?(Group) service_call end - def update_group_roles(work_package_member) + def update_group_roles(share) Groups::UpdateRolesService - .new(work_package_member.principal, - current_user: user, - contract_class: EmptyContract) - .call(member: work_package_member, - send_notifications: false, - message: nil) + .new(share.principal, current_user: user, contract_class: EmptyContract) + .call(member: share, send_notifications: false, message: nil) end end diff --git a/app/services/users/change_password_service.rb b/app/services/users/change_password_service.rb index b6ccf84b0a29..b2f5c41f7abc 100644 --- a/app/services/users/change_password_service.rb +++ b/app/services/users/change_password_service.rb @@ -65,7 +65,7 @@ def call(params) def invalidate_tokens ::Users::DropTokensService - .new(current_user: current_user) + .new(current_user:) .call! end diff --git a/app/services/users/drop_tokens_service.rb b/app/services/users/drop_tokens_service.rb index 098f397e9ad2..56f79cb75c1f 100644 --- a/app/services/users/drop_tokens_service.rb +++ b/app/services/users/drop_tokens_service.rb @@ -46,11 +46,11 @@ def call!(clear_invitation_tokens: true) private def invalidate_recovery_tokens - Token::Recovery.where(user: user).delete_all + Token::Recovery.where(user:).delete_all end def invalidate_invitation_tokens - Token::Invitation.where(user: user).delete_all + Token::Invitation.where(user:).delete_all end end end diff --git a/app/views/admin/settings/new_project_settings/show.html.erb b/app/views/admin/settings/new_project_settings/show.html.erb index 00cf388c8ba2..b1f60bb25c60 100644 --- a/app/views/admin/settings/new_project_settings/show.html.erb +++ b/app/views/admin/settings/new_project_settings/show.html.erb @@ -26,6 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> +<% html_title t(:label_administration), t(:label_project_new) %> <%= toolbar title: t(:label_project_new) %> <%= styled_form_tag(admin_settings_new_project_path, method: :patch) do %> diff --git a/app/views/admin/settings/project_custom_fields/edit.html.erb b/app/views/admin/settings/project_custom_fields/edit.html.erb index 31d4eadf1ce8..6d078413f512 100644 --- a/app/views/admin/settings/project_custom_fields/edit.html.erb +++ b/app/views/admin/settings/project_custom_fields/edit.html.erb @@ -26,6 +26,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> +<% html_title t(:label_administration), t("settings.project_attributes.heading"), @custom_field.name %> + <% content_controller 'admin--custom-fields', dynamic: true, 'admin--custom-fields-format-value': @custom_field.field_format diff --git a/app/views/admin/settings/project_custom_fields/new.html.erb b/app/views/admin/settings/project_custom_fields/new.html.erb index 143135f47ec5..92fd5270f350 100644 --- a/app/views/admin/settings/project_custom_fields/new.html.erb +++ b/app/views/admin/settings/project_custom_fields/new.html.erb @@ -31,6 +31,7 @@ See COPYRIGHT and LICENSE files for more details. 'admin--custom-fields-format-value': @custom_field.field_format %> +<% html_title t(:label_administration), t("settings.project_attributes.heading"), t('settings.project_attributes.new.heading') %> <% local_assigns[:additional_breadcrumb] = t('settings.project_attributes.new.heading') %> <%= render(Settings::ProjectCustomFields::NewFormHeaderComponent.new) %> diff --git a/app/views/admin/settings/project_custom_fields/project_mappings.html.erb b/app/views/admin/settings/project_custom_fields/project_mappings.html.erb index 7699c86ac2b9..ed01914788ca 100644 --- a/app/views/admin/settings/project_custom_fields/project_mappings.html.erb +++ b/app/views/admin/settings/project_custom_fields/project_mappings.html.erb @@ -37,10 +37,15 @@ See COPYRIGHT and LICENSE files for more details. <%= render(Primer::OpenProject::SubHeader.new) do |component| component.with_action_component do - render(Settings::ProjectCustomFields::ProjectCustomFieldMapping::NewProjectMappingComponent.new( - project_mapping: @project_mapping, - project_custom_field: @custom_field - )) + render(Primer::Beta::Button.new( + scheme: :primary, + tag: :a, + href: new_link_admin_settings_project_custom_field_path(@custom_field), + data: { controller: "async-dialog" } + )) do |button| + button.with_leading_visual_icon(icon: 'op-include-projects') + I18n.t("projects.settings.project_custom_fields.new_project_mapping_form.add_projects") + end end end unless @custom_field.required? %> diff --git a/app/views/admin/settings/projects_settings/show.html.erb b/app/views/admin/settings/projects_settings/show.html.erb index f5b2d89f3150..f8ca16c9be30 100644 --- a/app/views/admin/settings/projects_settings/show.html.erb +++ b/app/views/admin/settings/projects_settings/show.html.erb @@ -26,6 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> +<% html_title t(:label_administration), t(:label_project_list_plural) %> <%= toolbar title: t(:label_project_list_plural) %> <%= styled_form_tag(admin_settings_projects_path, method: :patch) do %> diff --git a/app/views/custom_fields/_form.html.erb b/app/views/custom_fields/_form.html.erb index f1b0039a02bf..bd686068abbb 100644 --- a/app/views/custom_fields/_form.html.erb +++ b/app/views/custom_fields/_form.html.erb @@ -224,8 +224,11 @@ See COPYRIGHT and LICENSE files for more details.
    - <%= f.check_box :searchable, - data: { 'admin--custom-fields-target': 'searchable' }%> + <%= f.check_box :searchable, + data: { 'admin--custom-fields-target': 'searchable' }%> +
    +

    <%= t('custom_fields.instructions.searchable') %>

    +
    <% when "TimeEntryCustomField" %>
    diff --git a/app/views/custom_styles/_inline_css_logo.erb b/app/views/custom_styles/_inline_css_logo.erb index c08ea9449440..904e75831c21 100644 --- a/app/views/custom_styles/_inline_css_logo.erb +++ b/app/views/custom_styles/_inline_css_logo.erb @@ -51,6 +51,10 @@ See COPYRIGHT and LICENSE files for more details. elsif CustomStyle.current.theme_logo.present? logo_url = asset_path(CustomStyle.current.theme_logo) end + + if isRu && logo_url == asset_path("logo_openproject.png") + logo_url = asset_path("logo-black-bg-ua.png") + end end %> diff --git a/app/views/custom_styles/_primer_color_mapping.erb b/app/views/custom_styles/_primer_color_mapping.erb index e43f54162ab9..11b96023fb16 100644 --- a/app/views/custom_styles/_primer_color_mapping.erb +++ b/app/views/custom_styles/_primer_color_mapping.erb @@ -18,6 +18,8 @@ --button-primary-bgColor-hover: var(--button--primary-background-hover-color) !important; --button-primary-bgColor-disabled: var(--button--primary-background-disabled-color) !important; --button-primary-borderColor-disabled: var(--button--primary-border-disabled-color) !important; + --fc-page-bg-color: var(--body-background) !important; + --fc-list-event-hover-bg-color: var(--control-transparent-bgColor-hover) !important; } /* Generic color mapping for content variables */ @@ -79,6 +81,7 @@ /* For dark themes we are using a lighter version of the accent and primary color. Otherwise they will not be seen */ [data-dark-theme=dark] { --accent-color: var(--accent-color--dark-mode); + --content-icon-color: var(--accent-color--dark-mode); --primary-button-color: var(--primary-button-color--dark-mode); --main-menu-bg-color: var(--overlay-bgColor); } diff --git a/app/views/groups/_memberships.html.erb b/app/views/groups/_memberships.html.erb index 03364ee2adc1..af0297e0ae3f 100644 --- a/app/views/groups/_memberships.html.erb +++ b/app/views/groups/_memberships.html.erb @@ -30,7 +30,7 @@ See COPYRIGHT and LICENSE files for more details. <% projects = Project.active.order(Arel.sql('lft')) %> <% memberships = @group.memberships %> -
    +
    <% if @group.memberships.any? %>
    diff --git a/app/views/groups/_users.html.erb b/app/views/groups/_users.html.erb index cb511e6762d4..4e7f11b2fe4d 100644 --- a/app/views/groups/_users.html.erb +++ b/app/views/groups/_users.html.erb @@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -
    +
    <% if @group.users.any? %> @@ -41,7 +41,7 @@ See COPYRIGHT and LICENSE files for more details. <% end %>
    -
    +
    <% if User.user .not_in_group(@group) .where(status: [User.statuses[:active], User.statuses[:invited]]) diff --git a/app/views/individual_principals/_memberships.html.erb b/app/views/individual_principals/_memberships.html.erb index ae758fff8472..1b4b91cc54c5 100644 --- a/app/views/individual_principals/_memberships.html.erb +++ b/app/views/individual_principals/_memberships.html.erb @@ -33,7 +33,7 @@ See COPYRIGHT and LICENSE files for more details. .order(Arel.sql('lft')) %> <% memberships = @individual_principal.memberships.where(id: Member.visible(current_user)) %> -
    +
    <% if memberships.any? %>
    diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index de20141c7b9f..a469acb90f2e 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -27,7 +27,6 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <% html_title(t(:label_project_plural)) -%> -
    <%= render( Projects::IndexPageHeaderComponent.new( @@ -38,13 +37,10 @@ See COPYRIGHT and LICENSE files for more details. ) ) %> - <%= render(Projects::IndexSubHeaderComponent.new(query:, current_user:, disable_buttons: state === :rename)) %> - <%= render Projects::TableComponent.new( query:, current_user: current_user, params:) %> - <%= render Projects::DiskUsageInformationComponent.new(current_user: current_user) %>
    diff --git a/app/views/work_packages/moves/new.html.erb b/app/views/work_packages/moves/new.html.erb index f1edbadcd240..f46a4af1b4f5 100644 --- a/app/views/work_packages/moves/new.html.erb +++ b/app/views/work_packages/moves/new.html.erb @@ -42,7 +42,7 @@ See COPYRIGHT and LICENSE files for more details. <% end -%> -<%= styled_form_tag({action: 'create'}, +<%= styled_form_tag({ action: 'create' }, id: 'move_form', data: { 'controller': 'refresh-on-form-changes', @@ -54,134 +54,147 @@ See COPYRIGHT and LICENSE files for more details. <%= hidden_field_tag 'ids[]', wp.id %> <% end %> <%= back_url_hidden_field_tag %> -
    -
    - <%= t(:label_change_properties) %> -
    -
    -
    - -
    - <%= angular_component_tag 'opce-project-autocompleter', - inputs: { - filters: [{ name: 'user_action', operator: '=', values: ['work_packages/move'] }], - inputName: 'new_project_id', - model: @target_project, - appendTo: 'body', - hiddenFieldAction: 'change->refresh-on-form-changes#triggerReload', - clearable: false, - }, - id: 'new_project_id', - class: 'remote-field--input', - data: { - 'test-selector': 'new_project_id' - } - %> -
    -
    -
    - -
    - <%= styled_select_tag("new_type_id", - content_tag('option', t(:label_no_change_option), value: '') + - options_from_collection_for_select(@types, "id", "name", @target_type&.id), - data: { - 'action': 'change->refresh-on-form-changes#triggerReload' - }) %> -
    +
    +
    + <%= t(:label_change_properties) %> +
    +
    +
    + +
    + <%= angular_component_tag 'opce-project-autocompleter', + inputs: { + filters: [{ name: 'user_action', operator: '=', values: ['work_packages/move'] }], + inputName: 'new_project_id', + model: @target_project, + appendTo: 'body', + hiddenFieldAction: 'change->refresh-on-form-changes#triggerReload', + clearable: false, + }, + id: 'new_project_id', + class: 'remote-field--input', + data: { + 'test-selector': 'new_project_id' + } + %>
    -
    - -
    - <%= styled_select_tag('status_id', - content_tag('option', t(:label_no_change_option), value: '') + - options_from_collection_for_select(@available_statuses, :id, :name)) %> -
    +
    +
    + +
    + <%= styled_select_tag("new_type_id", + content_tag('option', t(:label_no_change_option), value: '') + + options_from_collection_for_select(@types, "id", "name", @target_type&.id), + data: { + 'action': 'change->refresh-on-form-changes#triggerReload' + }) %>
    -
    - -
    - <%= styled_select_tag('version_id', - content_tag('option', t(:label_no_change_option), value: '') + - version_options_for_select(@available_versions)) %> + <% if @unavailable_type_in_target_project %> +
    +
    +

    + <% if @work_packages.size > 1 %> + <%= t("work_packages.move.bulk_current_type_not_available_in_target_project") %> + <% else %> + <%= t("work_packages.move.current_type_not_available_in_target_project") %> + <% end %> +

    +
    + <% end %> +
    +
    + +
    + <%= styled_select_tag('status_id', + content_tag('option', t(:label_no_change_option), value: '') + + options_from_collection_for_select(@available_statuses, :id, :name)) %>
    -
    - -
    - <%= styled_select_tag('priority_id', - content_tag('option', t(:label_no_change_option), value: '') + - options_from_collection_for_select(IssuePriority.active, :id, :name)) %> -
    +
    +
    + +
    + <%= styled_select_tag('version_id', + content_tag('option', t(:label_no_change_option), value: '') + + version_options_for_select(@available_versions)) %>
    -
    - -
    - <%= styled_select_tag('assigned_to_id', - content_tag('option', t(:label_no_change_option), value: '') + - content_tag('option', t(:label_nobody), value: 'none') + - options_from_collection_for_select(Principal.possible_assignee(@target_project), :id, :name)) %> -
    +
    +
    + +
    + <%= styled_select_tag('priority_id', + content_tag('option', t(:label_no_change_option), value: '') + + options_from_collection_for_select(IssuePriority.active, :id, :name)) %>
    -
    - -
    - <%= styled_select_tag('responsible_id', - content_tag('option', t(:label_no_change_option), value: '') + - content_tag('option', t(:label_nobody), value: 'none') + - options_from_collection_for_select(Principal.possible_assignee(@target_project), :id, :name)) %> -
    +
    +
    + +
    + <%= styled_select_tag('assigned_to_id', + content_tag('option', t(:label_no_change_option), value: '') + + content_tag('option', t(:label_nobody), value: 'none') + + options_from_collection_for_select(Principal.possible_assignee(@target_project), :id, :name)) %>
    -
    - <%= styled_label_tag :budget_id, Budget.model_name.human %> - <%= styled_select_tag('budget_id', (@target_project == @project ? content_tag('option', t(:label_no_change_option), :value => '') : "") + - content_tag('option', t(:label_none), :value => 'none') + - options_from_collection_for_select(@target_project.budgets, :id, :subject)) %> +
    +
    + +
    + <%= styled_select_tag('responsible_id', + content_tag('option', t(:label_no_change_option), value: '') + + content_tag('option', t(:label_nobody), value: 'none') + + options_from_collection_for_select(Principal.possible_assignee(@target_project), :id, :name)) %>
    -
    -
    - -
    - <%= angular_component_tag 'op-basic-single-date-picker', - inputs: { - id: "start_date", - name: "start_date" - } - %> -
    +
    + <%= styled_label_tag :budget_id, Budget.model_name.human %> + <%= styled_select_tag('budget_id', (@target_project == @project ? content_tag('option', t(:label_no_change_option), :value => '') : "") + + content_tag('option', t(:label_none), :value => 'none') + + options_from_collection_for_select(@target_project.budgets, :id, :subject)) %> +
    +
    +
    +
    + +
    + <%= angular_component_tag 'op-basic-single-date-picker', + inputs: { + id: "start_date", + name: "start_date" + } + %>
    -
    - -
    - <%= angular_component_tag 'op-basic-single-date-picker', - inputs: { - id: "due_date", - name: "due_date" - } - %> -
    +
    +
    + +
    + <%= angular_component_tag 'op-basic-single-date-picker', + inputs: { + id: "due_date", + name: "due_date" + } + %>
    - <% if @target_type %> - <% @target_type.custom_fields.required.each do |custom_field| %> -
    - <%= blank_custom_field_label_tag('', custom_field) %> -
    - <%= custom_field_tag_for_bulk_edit('', custom_field, @project) %> -
    +
    + <% if @target_type %> + <% @target_type.custom_fields.required.each do |custom_field| %> +
    + <%= blank_custom_field_label_tag('', custom_field) %> +
    + <%= custom_field_tag_for_bulk_edit('', custom_field, @project) %>
    - <% end %> +
    <% end %> -
    + <% end %>
    -
    -
    - <%= Journal.human_attribute_name(:notes) %> - <%= label_tag 'notes', Journal.human_attribute_name(:notes), class: 'hidden-for-sighted' %> - <%= styled_text_area_tag 'notes', @notes, cols: 60, rows: 10, class: 'wiki-edit', with_text_formatting: true %> -
    - <%= call_hook(:view_work_packages_move_bottom, work_packages: @work_packages, target_project: @target_project, copy: !!@copy) %> -
    +
    +
    +
    + <%= Journal.human_attribute_name(:notes) %> + <%= label_tag 'notes', Journal.human_attribute_name(:notes), class: 'hidden-for-sighted' %> + <%= styled_text_area_tag 'notes', @notes, cols: 60, rows: 10, class: 'wiki-edit', with_text_formatting: true %> +
    + <%= call_hook(:view_work_packages_move_bottom, work_packages: @work_packages, target_project: @target_project, copy: !!@copy) %> +
    <% if @copy %> <%= hidden_field_tag("copy") %> <%= styled_submit_tag t(:button_copy), class: '-primary' %> diff --git a/app/workers/debounceable_job.rb b/app/workers/debounceable_job.rb new file mode 100644 index 000000000000..1b4c94998ae8 --- /dev/null +++ b/app/workers/debounceable_job.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module DebounceableJob + # This module is generalizes the debounce logic that was originally used on {Storages::ManageStorageIntegrationsJob} + # Basically it ensures that a thread only queues one job per interval. + + # it depends on the class method `key` being implemented. The method will receive all the arguments + # used to invoke the job to construct the RequestStore key. + SINGLE_THREAD_DEBOUNCE_TIME = 4.seconds + + def debounce(*, **) + store_key = key(*, **) + timestamp = RequestStore.store[store_key] + + return false if timestamp.present? && (timestamp + SINGLE_THREAD_DEBOUNCE_TIME) > Time.current + + result = set(wait: 5.seconds).perform_later(*, **) + RequestStore.store[store_key] = Time.current + result + end +end diff --git a/app/workers/projects/export_job.rb b/app/workers/projects/export_job.rb index 08d649580fce..ccababb4a86c 100644 --- a/app/workers/projects/export_job.rb +++ b/app/workers/projects/export_job.rb @@ -7,7 +7,7 @@ class ExportJob < ::Exports::ExportJob private def prepare! - self.query = ::Queries::Projects::ProjectQuery.from_hash(query) + self.query = ::ProjectQuery.from_hash(query) end end end diff --git a/bin/dev b/bin/dev index 686781e2c899..3b0257b2678d 100755 --- a/bin/dev +++ b/bin/dev @@ -1,17 +1,10 @@ #!/usr/bin/env bash -function fallback_to_foreman() { - echo 'Overmind not installed. Falling back to foreman...' - if ! command -v foreman &> /dev/null; then - echo 'Installing foreman...' - gem install foreman - fi - - foreman start -f Procfile.dev -} - if command -v overmind &> /dev/null; then overmind start -f Procfile.dev +elif command -v foreman &> /dev/null; then + foreman start -f Procfile.dev else - fallback_to_foreman + echo 'Neither overmind, nor foreman is installed, either `gem install overmind` or `gem install foreman`.' + exit 1 fi diff --git a/config/constants/settings/definition.rb b/config/constants/settings/definition.rb index b0d79532a027..e5ce98023b93 100644 --- a/config/constants/settings/definition.rb +++ b/config/constants/settings/definition.rb @@ -434,7 +434,7 @@ class Definition }, enabled_projects_columns: { default: %w[favored name project_status public created_at latest_activity_at required_disk_space], - allowed: -> { Queries::Projects::ProjectQuery.new.available_selects.map { |s| s.attribute.to_s } } + allowed: -> { ProjectQuery.new.available_selects.map { |s| s.attribute.to_s } } }, enabled_scm: { default: %w[subversion git] @@ -525,7 +525,12 @@ class Definition default: 7.days }, host_name: { - default: "localhost:3000" + format: :string, + default: "localhost:3000", + default_by_env: { + # We do not want to set a localhost host name in production + production: nil + } }, hours_per_day: { description: "This will define what is considered a “day” when displaying duration in a more natural way " \ @@ -1032,7 +1037,7 @@ class Definition }, smtp_timeout: { format: :integer, - default: 5, + default: 5 }, software_name: { description: "Override software application name", @@ -1172,15 +1177,14 @@ class Definition def initialize(name, default:, + default_by_env: {}, description: nil, format: nil, writable: true, allowed: nil, env_alias: nil) self.name = name.to_s - @default = default.is_a?(Hash) ? default.deep_stringify_keys : default - @default.freeze - self.value = @default.dup + self.value = derive_default default_by_env.fetch(Rails.env.to_sym, default) self.format = format ? format.to_sym : deduce_format(value) self.writable = writable self.allowed = allowed @@ -1188,6 +1192,12 @@ def initialize(name, self.description = description.presence || :"setting_#{name}" end + def derive_default(default) + @default = default.is_a?(Hash) ? default.deep_stringify_keys : default + @default.freeze + @default.dup + end + def default cast(@default) end @@ -1276,6 +1286,7 @@ class << self # from the ENV OPENPROJECT_2FA as well. def add(name, default:, + default_by_env: {}, format: nil, description: nil, writable: true, @@ -1288,6 +1299,7 @@ def add(name, format:, description:, default:, + default_by_env:, writable:, allowed:, env_alias:) diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index 6a5e06c7a7e6..8b4ab0de9719 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -310,7 +310,8 @@ {}, permissible_on: :project, require: :loggedin, - dependencies: :view_work_packages + dependencies: :view_work_packages, + contract_actions: { queries: %i[create] } # Watchers wpt.permission :view_work_package_watchers, {}, @@ -330,8 +331,7 @@ map.permission :share_work_packages, { members: %i[destroy_by_principal], - "work_packages/shares": %i[index create destroy update resend_invite], - "work_packages/shares/bulk": %i[update destroy] + shares: %i[index create destroy update resend_invite bulk_update bulk_destroy] }, permissible_on: :project, dependencies: %i[edit_work_packages view_shared_work_packages], @@ -339,7 +339,7 @@ map.permission :view_shared_work_packages, { - "work_packages/shares": %i[index] + shares: %i[index] }, permissible_on: :project, require: :member, diff --git a/config/locales/crowdin/af.yml b/config/locales/crowdin/af.yml index 7384db720d4f..85f222fef3be 100644 --- a/config/locales/crowdin/af.yml +++ b/config/locales/crowdin/af.yml @@ -516,6 +516,10 @@ af: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ af: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ af: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3359,54 +3368,56 @@ af: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Rol" - type: "Soort" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Opmerking" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Redigeer" - edit_description: "Can view, comment and edit this work package." - view: "Bekyk" - view_description: "Can view this work package." - remove: "Remove" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/ar.yml b/config/locales/crowdin/ar.yml index df387a8a0fea..d5a9735e243d 100644 --- a/config/locales/crowdin/ar.yml +++ b/config/locales/crowdin/ar.yml @@ -544,6 +544,10 @@ ar: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "حجم النقل أو النسخ غير مدعوم من أجل حزم العمل المكونة من عدة مشاريع" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -943,6 +947,10 @@ ar: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2733,6 +2741,7 @@ ar: notice_principals_found_multiple: "وُجِد %{number} من النتائج.\nإضغط كي تحدد على النتيجة الأولى." notice_principals_found_single: "هنالك نتيجة واحدة.\nإضغط لتحديدها." notice_project_not_deleted: "لم يتم حذف المشروع." + notice_project_not_found: "Project not found." notice_successful_connection: "اتصال ناجح." notice_successful_create: "إنشاء ناجح." notice_successful_delete: "حذف ناجح." @@ -3501,54 +3510,56 @@ ar: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "الدور" - type: "النّوع" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "تعليق" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "تعديل" - edit_description: "Can view, comment and edit this work package." - view: "عرض" - view_description: "Can view this work package." - remove: "إزالة" - share: "شارك" - text_empty_search_description: "لا يوجد مستخدمون بالمعايير الحالية للتصفية." - text_empty_search_header: "لم نتمكن من العثور على أي نتائج مطابقة." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "غير مشترك" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "المستخدم %{user} مقفل ولا يمكن مشاركته" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/az.yml b/config/locales/crowdin/az.yml index dc43d1014515..28d8a34f0d33 100644 --- a/config/locales/crowdin/az.yml +++ b/config/locales/crowdin/az.yml @@ -516,6 +516,10 @@ az: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ az: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ az: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3359,54 +3368,56 @@ az: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Role" - type: "Type" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Comment" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Düzəliş et" - edit_description: "Can view, comment and edit this work package." - view: "View" - view_description: "Can view this work package." - remove: "Remove" - share: "Paylaş" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/be.yml b/config/locales/crowdin/be.yml index 93c7090e07c9..2de302cf251f 100644 --- a/config/locales/crowdin/be.yml +++ b/config/locales/crowdin/be.yml @@ -530,6 +530,10 @@ be: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -929,6 +933,10 @@ be: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2665,6 +2673,7 @@ be: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3431,54 +3440,56 @@ be: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Role" - type: "Type" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Comment" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Рэдагаваць" - edit_description: "Can view, comment and edit this work package." - view: "View" - view_description: "Can view this work package." - remove: "Remove" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/bg.yml b/config/locales/crowdin/bg.yml index e61265bd3151..d85ddec44e61 100644 --- a/config/locales/crowdin/bg.yml +++ b/config/locales/crowdin/bg.yml @@ -516,6 +516,10 @@ bg: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Масово преместване/копиране не се поддържа за работни пакети от множество проекти" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ bg: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ bg: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "Проектът не е изтрит." + notice_project_not_found: "Project not found." notice_successful_connection: "Успешна връзка." notice_successful_create: "Успешно създаване." notice_successful_delete: "Успешно изтриване." @@ -3359,54 +3368,56 @@ bg: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Роля" - type: "Тип" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Коментар" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Редактиране" - edit_description: "Can view, comment and edit this work package." - view: "Изглед" - view_description: "Can view this work package." - remove: "Премахване" - share: "Споделяне" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Не е споделен" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/ca.yml b/config/locales/crowdin/ca.yml index 5a6ea88b28e7..dc9ea73f6685 100644 --- a/config/locales/crowdin/ca.yml +++ b/config/locales/crowdin/ca.yml @@ -512,6 +512,10 @@ ca: move: no_common_statuses_exists: "No hi ha estats disponibles per a tots els paquets de treball seleccionats. El seus estats no es poden canviar." unsupported_for_multiple_projects: "Moure o copiar en massa no està soportat per paquets de treball de múltiples projectes" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -911,6 +915,10 @@ ca: enabled_modules: dependency_missing: "El mòdul \"%{dependency}\" necessita ser activat també, ja que el mòdul \"%{module}\" en depèn." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2590,6 +2598,7 @@ ca: notice_principals_found_multiple: "S'han trobat %{number} resultats.\nPrem per focalitzar el primer resultat." notice_principals_found_single: "S'han trobat un resultat.\nPrem per focalitzar-lo." notice_project_not_deleted: "El projecte no s'ha suprimit." + notice_project_not_found: "Project not found." notice_successful_connection: "S'ha connectat correctament." notice_successful_create: "Creat correctament." notice_successful_delete: "Esborrat correctament." @@ -3348,54 +3357,56 @@ ca: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Rol" - type: "Tipus" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Comentari" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Editar" - edit_description: "Can view, comment and edit this work package." - view: "Mostra" - view_description: "Can view this work package." - remove: "Suprimir" - share: "Compartir" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "No compartit" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/ckb-IR.yml b/config/locales/crowdin/ckb-IR.yml index e53a1b59232b..4e71d0ac15c9 100644 --- a/config/locales/crowdin/ckb-IR.yml +++ b/config/locales/crowdin/ckb-IR.yml @@ -516,6 +516,10 @@ ckb-IR: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ ckb-IR: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ ckb-IR: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3359,54 +3368,56 @@ ckb-IR: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Role" - type: "Type" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Comment" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Edit" - edit_description: "Can view, comment and edit this work package." - view: "View" - view_description: "Can view this work package." - remove: "Remove" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/cs.yml b/config/locales/crowdin/cs.yml index a7b2a14f55da..c7cda35b0abc 100644 --- a/config/locales/crowdin/cs.yml +++ b/config/locales/crowdin/cs.yml @@ -530,6 +530,10 @@ cs: move: no_common_statuses_exists: "Pro všechny vybrané pracovní balíčky není k dispozici žádný stav. Jejich stav nelze změnit." unsupported_for_multiple_projects: "Hromadný přesun/kopírování není podporováno pro pracovní balíčky z více projektů" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -929,6 +933,10 @@ cs: enabled_modules: dependency_missing: "Modul '%{dependency}' musí být také povolen, protože modul '%{module}' na něm závisí." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2664,6 +2672,7 @@ cs: notice_principals_found_multiple: "Nalezeno %{number} výsledků. \nStiskněte Tab pro přechod na první výsledek." notice_principals_found_single: "Existuje jeden výsledek. \n Záložka pro zaměření." notice_project_not_deleted: "Projekt nebyl odstraněn." + notice_project_not_found: "Project not found." notice_successful_connection: "Úspěšné připojení." notice_successful_create: "Úspěšné vytvoření." notice_successful_delete: "Úspěšné odstranění." @@ -3429,54 +3438,56 @@ cs: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 uživatelů" - one: "1 uživatel" - other: "%{count} uživatelů" - filter: - project_member: "Člen projektu" - not_project_member: "Není členem projektu" - project_group: "Skupina projektů" - not_project_group: "Není skupina projektů" - role: "Role" - type: "Typ" - label_search: "Hledat uživatele pro pozvání" - label_search_placeholder: "Hledat podle uživatele nebo e-mailové adresy" - label_toggle_all: "Přepnout všechny sdílené" - permissions: - comment: "Komentář" - comment_description: "Může zobrazit a komentovat tento pracovní balíček." - denied: "Nemáte oprávnění ke sdílení pracovních balíčků." - edit: "Upravit" - edit_description: "Může zobrazovat, komentovat a upravovat tento pracovní balíček." - view: "Zobrazit" - view_description: "Může zobrazit tento pracovní balíček." - remove: "Odebrat" - share: "Sdílet" - text_empty_search_description: "Neexistují žádní uživatelé s aktuálními kritérii filtru." - text_empty_search_header: "Nenašli jsme žádné odpovídající výsledky." - text_empty_state_description: "Pracovní balíček zatím nebyl s nikým sdílen." - text_empty_state_header: "Není sdíleno" - text_user_limit_reached: "Přidáním dalších uživatelů bude aktuální limit překročen. Pro zvýšení limitu uživatelů kontaktujte správce, abyste zajistili přístup externích uživatelů k tomuto pracovnímu balíčku." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Přidáním dalších uživatelů bude aktuální limit překročen. Pro zvýšení limitu uživatelů kontaktujte správce, abyste zajistili přístup externích uživatelů k tomuto pracovnímu balíčku. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Vyberte uživatele pro sdílení tohoto pracovního balíčku " - warning_locked_user: "Uživatel %{user} je uzamčen a nemůže být s ním sdíleno" - user_details: - locked: "Uzamčený uživatel" - invited: "Pozvánka odeslána" - resend_invite: "Znovu odeslat." - invite_resent: "Pozvánka byla znovu odeslána" - not_project_member: "Není členem projektu" - project_group: "Členové skupiny mohou mít dodatečná práva (jako členové projektu)" - not_project_group: "Skupina (sdílená se všemi členy)" - additional_privileges_project: "Může mít další oprávnění (jako člen projektu)" - additional_privileges_group: "Může mít dodatečná oprávnění (jako člen skupiny)" - additional_privileges_project_or_group: "Může mít dodatečná oprávnění (jako člen skupiny)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/da.yml b/config/locales/crowdin/da.yml index 913c3762cabc..1f5ec50d435b 100644 --- a/config/locales/crowdin/da.yml +++ b/config/locales/crowdin/da.yml @@ -514,6 +514,10 @@ da: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -913,6 +917,10 @@ da: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2595,6 +2603,7 @@ da: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "Projektet blev ikke slettet." + notice_project_not_found: "Project not found." notice_successful_connection: "Forbindelse gennemført." notice_successful_create: "Oprettelse gennemført." notice_successful_delete: "Sletning gennemført." @@ -3355,54 +3364,56 @@ da: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Rolle" - type: "Type" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Kommentér" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Rediger" - edit_description: "Can view, comment and edit this work package." - view: "Se" - view_description: "Can view this work package." - remove: "Fjern" - share: "Del" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Ikke delt" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/de.yml b/config/locales/crowdin/de.yml index 47223e94d199..88d623cc2bac 100644 --- a/config/locales/crowdin/de.yml +++ b/config/locales/crowdin/de.yml @@ -510,6 +510,10 @@ de: move: no_common_statuses_exists: "Es gibt keine gemeinsamen Status für die ausgewählten Arbeitspakete. Ihr Status kann daher nicht verändert werden." unsupported_for_multiple_projects: "Verschieben / Kopieren nicht unterstützt für Arbeitspakete in verschiedenen Projekten" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Der Workflow für das Teilen von Arbeitspaketen fehlt" @@ -909,6 +913,10 @@ de: enabled_modules: dependency_missing: "Das Modul \"%{dependency}\" muss ebenfalls aktiviert sein, da das Modul \"%{module}\" dieses benötigt." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Bitte wählen Sie ein Projekt aus." query: attributes: project: @@ -2591,6 +2599,7 @@ de: notice_principals_found_multiple: "Es wurden %{number} Ergebnisse gefunden.\nDrücke Tab um das erste Ergebnis zu fokussieren." notice_principals_found_single: "Es wurde ein Ergebnis gefunden.\nDrücke Tab um es zu fokussieren." notice_project_not_deleted: "Das Projekt wurde nicht gelöscht." + notice_project_not_found: "Projekt nicht gefunden." notice_successful_connection: "Verbindung erfolgreich." notice_successful_create: "Erfolgreich angelegt." notice_successful_delete: "Erfolgreich gelöscht." @@ -3352,54 +3361,56 @@ de: work_based_help_text: "% Abgeschlossen wird automatisch aus Aufwand und Verbleibender Aufwand abgeleitet." status_based_help_text: "% Abgeschlossen wird durch den Status des Arbeitspakets festgelegt." migration_warning_text: "Im aufwandsbezogenen Modus, kann % Fertig nicht manuell eingegeben werden und ist immer an den Aufwand gebunden. Der vorhandene Wert wurde beibehalten, kann aber nicht bearbeitet werden. Bitte geben Sie zuerst den Wert für Aufwand ein." - sharing: - count: - zero: "0 Benutzer" - one: "1 Benutzer" - other: "%{count} Benutzer" - filter: - project_member: "Projektmitglied" - not_project_member: "Kein Projektmitglied" - project_group: "Projektgruppe" - not_project_group: "Keine Projektgruppe" - role: "Rolle" - type: "Typ" - label_search: "Suche nach einzuladenen Benutzern" - label_search_placeholder: "Suche nach Benutzer oder E-Mail-Adresse" - label_toggle_all: "Alle Freigaben umschalten" - permissions: - comment: "Kommentar" - comment_description: "Kann dieses Arbeitspaket anzeigen und kommentieren." - denied: "Sie haben keine Berechtigung, Arbeitspakete zu teilen." - edit: "Bearbeiten" - edit_description: "Kann dieses Arbeitspaket ansehen, kommentieren und editieren." - view: "Ansicht" - view_description: "Kann dieses Arbeitspaket ansehen." - remove: "Entfernen" - share: "Teilen" - text_empty_search_description: "Es gibt keine Benutzer mit den aktuellen Filterkriterien." - text_empty_search_header: "Keine passenden Ergebnisse gefunden." - text_empty_state_description: "Das Arbeitspaket wurde mit niemandem geteilt." - text_empty_state_header: "Nicht geteilt" - text_user_limit_reached: "Das Hinzufügen zusätzlicher Benutzer überschreitet das aktuelle Benutzerlimit. Bitte kontaktieren Sie einen Administrator, um sicherzustellen, dass externe Benutzer auf diese Instanz zugreifen können." - text_user_limit_reached_admins: 'Das Hinzufügen zusätzlicher Benutzer überschreitet das aktuelle Benutzerlimit. Bitte aktualisieren Sie Ihr Abonnement um sicherzustellen, dass externe Benutzer auf diese Instanz zugreifen können.' - warning_user_limit_reached: > - Das Hinzufügen zusätzlicher Benutzer überschreitet das aktuelle Benutzerlimit. Bitte kontaktieren Sie einen Administrator, um sicherzustellen, dass externe Benutzer auf diese Instanz zugreifen können. - warning_user_limit_reached_admin: > - Das Hinzufügen zusätzlicher Benutzer überschreitet das aktuelle Benutzerlimit. Bitte aktualisieren Sie Ihr Abonnement um sicherzustellen, dass externe Benutzer auf diese Instanz zugreifen können. - warning_no_selected_user: "Bitte wählen Sie Benutzer aus, mit denen dieses Arbeitspaket geteilt werden soll" - warning_locked_user: "Der Benutzer %{user} ist gesperrt und mit ihm kann nicht geteilt werden" - user_details: - locked: "Gesperrte Benutzer" - invited: "Einladung gesendet. " - resend_invite: "Erneut senden." - invite_resent: "Einladung wurde versendet" - not_project_member: "Kein Projektmitglied" - project_group: "Gruppenmitglieder haben möglicherweise zusätzliche Berechtigungen (als Projektmitglieder)" - not_project_group: "Gruppe (Teilen mit allen Mitgliedern)" - additional_privileges_project: "hat möglicherweise zusätzliche Berechtigungen (als Projektmitglied)" - additional_privileges_group: "hat möglicherweise zusätzliche Berechtigungen (als Gruppenmitglied)" - additional_privileges_project_or_group: "hat möglicherweise zusätzliche Berechtigungen (als Projekt- oder Gruppenmitglied)" + permissions: + comment: "Kommentar" + comment_description: "Can view and comment this work package." + edit: "Bearbeiten" + edit_description: "Can view, comment and edit this work package." + view: "Ansicht" + view_description: "Kann dieses Arbeitspaket ansehen." + sharing: + count: + zero: "0 Benutzer" + one: "1 Benutzer" + other: "%{count} Benutzer" + filter: + project_member: "Projektmitglied" + not_project_member: "Kein Projektmitglied" + project_group: "Projektgruppe" + not_project_group: "Keine Projektgruppe" + user: "Benutzer" + group: "Gruppe" + role: "Rolle" + type: "Typ" + denied: "Sie haben keine Berechtigung, %{entities} zu teilen." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Alle Freigaben umschalten" + remove: "Entfernen" + share: "Teilen" + text_empty_search_description: "Es gibt keine Benutzer mit den aktuellen Filterkriterien." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Nicht geteilt" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Gesperrter Benutzer" + invited: "Einladung gesendet. " + resend_invite: "Erneut senden." + invite_resent: "Einladung wurde erneut gesendet" + not_project_member: "Kein Projektmitglied" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Tage, die nicht ausgewählt sind, werden bei der Planung von Arbeitspaketen übersprungen (und bei der Tageszählung nicht berücksichtigt). Diese können auf Arbeitspaket-Ebene überschrieben werden. diff --git a/config/locales/crowdin/el.yml b/config/locales/crowdin/el.yml index be6aa253940a..ecefc4bd6490 100644 --- a/config/locales/crowdin/el.yml +++ b/config/locales/crowdin/el.yml @@ -512,6 +512,10 @@ el: move: no_common_statuses_exists: "Δεν υπάρχει διαθέσιμη κατάσταση για όλα τα επιλεγμένα πακέτα εργασίας. Η κατάσταση τους δεν μπορεί να αλλάξει." unsupported_for_multiple_projects: "Η μαζική μετακίνηση/αντιγραφή δεν υποστηρίζεται για πακέτα εργασίας από πολλά έργα" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -911,6 +915,10 @@ el: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2592,6 +2600,7 @@ el: notice_principals_found_multiple: "Βρέθηκαν %{number} αποτελέσματα. \n Πατήστε Tab για να εστιάσετε στο πρώτο αποτέλεσμα." notice_principals_found_single: "Υπάρχει ένα αποτέλεσμα. \n Πατήστε Tab για να εστιάσετε σε αυτό." notice_project_not_deleted: "Το έργο δεν διαγράφηκε." + notice_project_not_found: "Project not found." notice_successful_connection: "Επιτυχής σύνδεση." notice_successful_create: "Επιτυχής δημιουργία." notice_successful_delete: "Επιτυχής διαγραφή." @@ -3353,54 +3362,56 @@ el: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Ρόλος" - type: "Τύπος" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Σχόλιο" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Επεξεργασία" - edit_description: "Can view, comment and edit this work package." - view: "Προβολή" - view_description: "Can view this work package." - remove: "Αφαίρεση" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Δεν μοιράζονται" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/eo.yml b/config/locales/crowdin/eo.yml index e4c038904a3c..f130cdf95b8c 100644 --- a/config/locales/crowdin/eo.yml +++ b/config/locales/crowdin/eo.yml @@ -516,6 +516,10 @@ eo: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ eo: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ eo: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3359,54 +3368,56 @@ eo: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Rolo" - type: "Tipo" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Komento" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Redakti" - edit_description: "Can view, comment and edit this work package." - view: "Montri" - view_description: "Can view this work package." - remove: "Forigi" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/es.seeders.yml b/config/locales/crowdin/es.seeders.yml index 44182bb014ff..49e5e81aae4f 100644 --- a/config/locales/crowdin/es.seeders.yml +++ b/config/locales/crowdin/es.seeders.yml @@ -43,9 +43,9 @@ es: name: Otros project_query_roles: item_0: - name: Project query viewer + name: Visor de vistas de proyecto item_1: - name: Project query editor + name: Editor de vistas de proyecto work_package_roles: item_0: name: Editor de paquetes de trabajo diff --git a/config/locales/crowdin/es.yml b/config/locales/crowdin/es.yml index f1a37a9e0b46..432922178ac5 100644 --- a/config/locales/crowdin/es.yml +++ b/config/locales/crowdin/es.yml @@ -263,7 +263,7 @@ es: my: "Mis proyectos" favored: "Proyectos favoritos" archived: "Proyectos archivados" - public: "Lista de proyectos públicos" + public: "Lista públicas de proyectos" my_private: "Mis listas de proyectos privados" new: placeholder: "Nueva lista de proyectos" @@ -465,7 +465,7 @@ es: is_readonly: "Solo lectura" excluded_from_totals: "Excluido de totales" themes: - dark: "Dark" + dark: "Oscuro" light: "Claro" light_high_contrast: "Contraste alto claro" types: @@ -513,6 +513,10 @@ es: move: no_common_statuses_exists: "No hay ningún estado disponible para todos los paquetes de trabajo seleccionados. Su estado no se puede cambiar." unsupported_for_multiple_projects: "Mover o copiar por lotes no esta soportado para paquetes de trabajo de múltiples proyectos" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Falta el flujo de trabajo para compartir paquetes de trabajo" @@ -912,6 +916,10 @@ es: enabled_modules: dependency_missing: "El módulo «%{dependency}» debe habilitarse también, ya que el módulo «%{module}» depende de este." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -936,8 +944,8 @@ es: nonexistent: "La columna «%{column}» no existe." format: "%{message}" group_by_hierarchies_exclusive: "es mutuamente exclusivo con el grupo '%{group_by}'. No puede activar ambos." - can_only_be_modified_by_owner: "La consulta sólo puede ser modificada por su propietario." - need_permission_to_modify_public_query: "No puede modificar una consulta pública." + can_only_be_modified_by_owner: "La vista sólo puede ser modificada por su propietario." + need_permission_to_modify_public_query: "No puede modificar una vista pública." filters: custom_fields: inexistent: "No hay ningún campo personalizado para el filtro." @@ -1338,8 +1346,8 @@ es: button_revoke_access: "Revocar acceso" button_revoke_all: "Revocar todos" button_revoke_only: "Revocar solo %{shared_role_name}" - button_publish: "Hacer público" - button_unpublish: "Hacer privado" + button_publish: "Hacer pública" + button_unpublish: "Hacer privada" consent: checkbox_label: He notado y doy mi consentimiento a lo anterior. failure_message: Consentimiento fallido, no puede continuar. @@ -2594,6 +2602,7 @@ es: notice_principals_found_multiple: "Hemos encontrado %{number} resultados. \n Pulse TAB para ver el primer resultado." notice_principals_found_single: "Se ha encontrado solo un resultado. \n Pulse TAB para verlo." notice_project_not_deleted: "El proyecto no fue eliminado." + notice_project_not_found: "Project not found." notice_successful_connection: "Conexión exitosa." notice_successful_create: "Creación exitosa." notice_successful_delete: "Eliminado con éxito." @@ -2728,9 +2737,9 @@ es: permission_save_bcf_queries: "Guardar consultas BCF" permission_manage_public_bcf_queries: "Administrar consultas BCF públicas" permission_edit_attribute_help_texts: "Editar textos de ayuda de atributos" - permission_manage_public_project_queries: "Administrar las listas públicas de proyectos" - permission_view_project_query: "View project query" - permission_edit_project_query: "Edit project query" + permission_manage_public_project_queries: "Administrar listas públicas de proyectos" + permission_view_project_query: "Ver vistas de proyecto" + permission_edit_project_query: "Editar vistas de proyecto" placeholders: default: "-" project: @@ -3354,54 +3363,56 @@ es: work_based_help_text: "El % completado se obtiene automáticamente a partir del Trabajo y del Trabajo restante." status_based_help_text: "% completado se establece por el estado del paquete de trabajo." migration_warning_text: "En el modo de cálculo del progreso basado en el trabajo, el % completado no puede fijarse manualmente y está vinculado al Trabajo. El valor existente se mantiene, pero no puede editarse. Introduzca primero el Trabajo." - sharing: - count: - zero: "0 usuarios" - one: "1 usuario" - other: "%{count} usuarios" - filter: - project_member: "Miembro del proyecto" - not_project_member: "No miembro del proyecto" - project_group: "Grupo del proyecto" - not_project_group: "No grupo del proyecto" - role: "Perfil" - type: "Tipo" - label_search: "Buscar usuarios para invitar" - label_search_placeholder: "Buscar por usuario o dirección de correo electrónico" - label_toggle_all: "Activar todas las comparticiones" - permissions: - comment: "Comentario" - comment_description: "Puede ver y comentar este paquete de trabajo." - denied: "No tiene permisos para compartir paquetes de trabajo." - edit: "Editar" - edit_description: "Puede ver, comentar y editar este paquete de trabajo." - view: "Ver" - view_description: "Puede ver este paquete de trabajo." - remove: "Eliminar" - share: "Compartir" - text_empty_search_description: "No hay usuarios con los criterios de filtro actuales." - text_empty_search_header: "No pudimos encontrar ningún resultado coincidente." - text_empty_state_description: "El paquete de trabajo aún no ha sido compartido con nadie." - text_empty_state_header: "No compartido" - text_user_limit_reached: "Añadir usuarios adicionales excederá el límite actual. Póngase en contacto con un administrador para aumentar el límite de usuario para asegurar que los usuarios externos puedan acceder a este paquete de trabajo." - text_user_limit_reached_admins: 'Añadir usuarios adicionales excederá el límite actual. Por favor, actualice su plan para poder añadir más usuarios.' - warning_user_limit_reached: > - Añadir usuarios adicionales excederá el límite actual. Póngase en contacto con un administrador para aumentar el límite de usuario para asegurar que los usuarios externos puedan acceder a este paquete de trabajo. - warning_user_limit_reached_admin: > - Añadir usuarios adicionales excederá el límite actual. Por favor, actualice su plan para asegurar que los usuarios externos puedan acceder a este paquete de trabajo. - warning_no_selected_user: "Por favor, seleccione usuarios para compartir este paquete de trabajo" - warning_locked_user: "él usuario %{user} está bloqueado y no se puede compartir con él" - user_details: - locked: "Usuario bloqueado" - invited: "Invitación enviada. " - resend_invite: "Reenviar." - invite_resent: "La invitación ha sido reenviada" - not_project_member: "No miembro del proyecto" - project_group: "Los miembros del grupo pueden tener privilegios adicionales (como miembros del proyecto)" - not_project_group: "Grupo (compartido con todos los miembros)" - additional_privileges_project: "Puede tener privilegios adicionales (como miembros del proyecto)" - additional_privileges_group: "Puede tener privilegios adicionales (como miembros de un grupo)" - additional_privileges_project_or_group: "Puede tener privilegios adicionales (como miembros del proyecto o de un grupo)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Los días no seleccionados se omiten al programar los paquetes de trabajo (y no se incluyen en el recuento de días). Esto puede anularse a nivel de paquete de trabajo. diff --git a/config/locales/crowdin/et.yml b/config/locales/crowdin/et.yml index cae58e9f6660..fe5404b10a77 100644 --- a/config/locales/crowdin/et.yml +++ b/config/locales/crowdin/et.yml @@ -516,6 +516,10 @@ et: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ et: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ et: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "Projekti ei ole kustutatud." + notice_project_not_found: "Project not found." notice_successful_connection: "Ühenduse loomine õnnestus." notice_successful_create: "Loomine õnnestus." notice_successful_delete: "Kustutamine õnnestus." @@ -3359,54 +3368,56 @@ et: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Roll" - type: "Tüüp" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Kommentaar" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Muuda" - edit_description: "Can view, comment and edit this work package." - view: "Kuva" - view_description: "Can view this work package." - remove: "Eemalda" - share: "Jaga" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Ei jaga" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/eu.yml b/config/locales/crowdin/eu.yml index 6fecd32af07c..197debe060eb 100644 --- a/config/locales/crowdin/eu.yml +++ b/config/locales/crowdin/eu.yml @@ -516,6 +516,10 @@ eu: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ eu: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ eu: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3359,54 +3368,56 @@ eu: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Role" - type: "Type" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Comment" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Edit" - edit_description: "Can view, comment and edit this work package." - view: "View" - view_description: "Can view this work package." - remove: "Remove" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/fa.yml b/config/locales/crowdin/fa.yml index b73b3d8b7537..f492ba158a50 100644 --- a/config/locales/crowdin/fa.yml +++ b/config/locales/crowdin/fa.yml @@ -516,6 +516,10 @@ fa: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ fa: enabled_modules: dependency_missing: "با توجه به وابستگی %{module} به افزونه %{dependency}، این افزونه نیز بایستی فعال شود." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ fa: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3359,54 +3368,56 @@ fa: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Role" - type: "نوع" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "نظر" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "ویرایش" - edit_description: "Can view, comment and edit this work package." - view: "مشاهده" - view_description: "Can view this work package." - remove: "Remove" - share: "به اشتراک گذاری" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/fi.yml b/config/locales/crowdin/fi.yml index 9389375c74c7..840226f4f0e6 100644 --- a/config/locales/crowdin/fi.yml +++ b/config/locales/crowdin/fi.yml @@ -516,6 +516,10 @@ fi: move: no_common_statuses_exists: "Kaikkien valittujen tehtävien tilaa ei ole saatavilla. Niiden tilaa ei voi muuttaa." unsupported_for_multiple_projects: "Massasiirto tai -kopiointi ei ole sallittu eri projektien tehtäville" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ fi: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ fi: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Yhteyden muodostus onnistui." notice_successful_create: "Luonti onnistui." notice_successful_delete: "Poisto onnistui." @@ -3359,54 +3368,56 @@ fi: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Rooli" - type: "Tyyppi" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Kommentti" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Muokkaa" - edit_description: "Can view, comment and edit this work package." - view: "Näytä" - view_description: "Can view this work package." - remove: "Poista" - share: "Jaa" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Ei ole jaettu" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/fil.yml b/config/locales/crowdin/fil.yml index d1bb134c72e2..dc01e0078fb8 100644 --- a/config/locales/crowdin/fil.yml +++ b/config/locales/crowdin/fil.yml @@ -516,6 +516,10 @@ fil: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Ang bulk na paglipat/kopya ay hindi suportado sa mga work packages mula sa iba't ibang mga proyekto" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ fil: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ fil: notice_principals_found_multiple: "Mayroong %{number} mga resulta ang natagpuan.\n Ang tab ay tumuon sa unang resulta." notice_principals_found_single: "Mayroong isang resulta. \n Tab upang ituon ito." notice_project_not_deleted: "Ang proyekto ay hindi nabura." + notice_project_not_found: "Project not found." notice_successful_connection: "Matagumpay na ikonekta." notice_successful_create: "Matagumpay pagkalikha." notice_successful_delete: "Matagumpay ang pagtanggal." @@ -3357,54 +3366,56 @@ fil: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Tungkulin" - type: "Uri" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Komento" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "I-edit" - edit_description: "Can view, comment and edit this work package." - view: "Tingnan" - view_description: "Can view this work package." - remove: "Tanggalin" - share: "Ibahagi" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Hindi ibinahagi" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/fr.yml b/config/locales/crowdin/fr.yml index 238bea876511..c46e274a51c2 100644 --- a/config/locales/crowdin/fr.yml +++ b/config/locales/crowdin/fr.yml @@ -515,6 +515,10 @@ fr: move: no_common_statuses_exists: "Il n’y a pas de statut disponible pour tous les lots de travaux sélectionnés. Ce statut ne peut pas être changé." unsupported_for_multiple_projects: "Déplacer/copier en masse n'est pas supporté pour des lots de travaux provenant de plusieurs projets" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Flux de travail manquant pour le partage de lots de travaux" @@ -914,6 +918,10 @@ fr: enabled_modules: dependency_missing: "Le module « %{dependency} » doit également être activé car le module « %{module} » en dépend." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2596,6 +2604,7 @@ fr: notice_principals_found_multiple: "Il y a %{number} résultats trouvés. Tabulation pour sélectionner le premier résultat." notice_principals_found_single: "Il y a un seul résultat. Tabulation pour le sélectionner." notice_project_not_deleted: "Le projet n'a pas été supprimé." + notice_project_not_found: "Project not found." notice_successful_connection: "Connection réussie." notice_successful_create: "Création réussie." notice_successful_delete: "Suppression réussie." @@ -3357,54 +3366,56 @@ fr: work_based_help_text: "Le % réalisé est automatiquement dérivé de Travail et Travail restant." status_based_help_text: "Le % réalisé est défini par le statut du lot de travaux." migration_warning_text: "Dans le mode de calcul de la progression basé sur le travail, le % réalisé ne peut pas être défini manuellement et est lié au travail. La valeur existante a été conservée mais ne peut pas être modifiée. Veuillez d'abord renseigner Travail." - sharing: - count: - zero: "0 utilisateur" - one: "1 utilisateur" - other: "%{count} utilisateurs" - filter: - project_member: "Membre du projet" - not_project_member: "Non membre du projet" - project_group: "Groupe de projet" - not_project_group: "Non groupe du projet" - role: "Rôle" - type: "Type" - label_search: "Rechercher des utilisateurs à inviter" - label_search_placeholder: "Rechercher par nom ou par adresse mail..." - label_toggle_all: "Activer/désactiver tous les partages" - permissions: - comment: "Commentaire" - comment_description: "Peut voir et commenter ce lot de travaux." - denied: "Vous n'avez pas l'autorisation de partager des lots de travaux. " - edit: "Éditer" - edit_description: "Peut voir, commenter et modifier ce lot de travaux." - view: "Afficher" - view_description: "Peut voir ce lot de travaux." - remove: "Supprimer" - share: "Partager" - text_empty_search_description: "Aucun utilisateur ne répond aux critères de filtrage actuels." - text_empty_search_header: "Aucun résultat correspondant n'a été trouvé." - text_empty_state_description: "Le lot de travaux n'a encore été partagé avec personne." - text_empty_state_header: "Non partagé" - text_user_limit_reached: "L'ajout d'utilisateurs supplémentaires dépassera la limite actuelle. Veuillez contacter un administrateur pour augmenter la limite d'utilisateurs afin que les utilisateurs externes puissent accéder à ce lot de travaux." - text_user_limit_reached_admins: 'L''ajout d''utilisateurs supplémentaires dépassera la limite actuelle. Veuillez mettre à niveau votre plan pour pouvoir ajouter d''autres utilisateurs.' - warning_user_limit_reached: > - L'ajout d'utilisateurs supplémentaires dépassera la limite actuelle. Veuillez contacter un administrateur pour augmenter la limite d'utilisateurs afin de garantir que les utilisateurs externes puissent accéder à ce work package. - warning_user_limit_reached_admin: > - L'ajout d'utilisateurs supplémentaires dépassera la limite actuelle. Veuillez mettre à niveau votre plan pour que les utilisateurs externes puissent accéder à ce work package. - warning_no_selected_user: "Veuillez sélectionner les utilisateurs avec lesquels partager ce lot de travaux" - warning_locked_user: "Impossible de partager avec %{user} dont le compte est verrouillé" - user_details: - locked: "Utilisateur verrouillé" - invited: "Invitation envoyée." - resend_invite: "Renvoyer." - invite_resent: "L'Invitation a été renvoyée." - not_project_member: "Non membre du projet" - project_group: "Les membres du groupe peuvent avoir des privilèges supplémentaires (en tant que membres du projet)" - not_project_group: "" - additional_privileges_project: "Peut avoir des privilèges supplémentaires (en tant que membre du projet)" - additional_privileges_group: "Peut avoir des privilèges supplémentaires (en tant que membre d'un group)" - additional_privileges_project_or_group: "Peut avoir des privilèges supplémentaires (en tant que membre du projet ou d'un groupe)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Les jours qui ne sont pas sélectionnés sont ignorés lors de la planification des lots de travaux (et non inclus dans le nombre de jours). Ils peuvent être remplacés au niveau du lot de travaux. diff --git a/config/locales/crowdin/he.yml b/config/locales/crowdin/he.yml index 26dc92951289..b63f85bb03a2 100644 --- a/config/locales/crowdin/he.yml +++ b/config/locales/crowdin/he.yml @@ -530,6 +530,10 @@ he: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -929,6 +933,10 @@ he: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2665,6 +2673,7 @@ he: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3431,54 +3440,56 @@ he: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "תפקיד" - type: "סוג" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "תגובה" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "עריכה" - edit_description: "Can view, comment and edit this work package." - view: "תצוגה" - view_description: "Can view this work package." - remove: "Remove" - share: "שתף" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/hi.yml b/config/locales/crowdin/hi.yml index f80f5b097930..58f26cb53665 100644 --- a/config/locales/crowdin/hi.yml +++ b/config/locales/crowdin/hi.yml @@ -514,6 +514,10 @@ hi: move: no_common_statuses_exists: "सभी चयनित कार्य पैकेज के लिए कोई स्थिति उपलब्ध नहीं है । उनकी हैसियत नहीं बदली जा सकती ।" unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -913,6 +917,10 @@ hi: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2595,6 +2603,7 @@ hi: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3357,54 +3366,56 @@ hi: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "भूमिका" - type: "प्रकार" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "टिप्पणी" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "संपादित करें" - edit_description: "Can view, comment and edit this work package." - view: "दृश्य" - view_description: "Can view this work package." - remove: "Remove" - share: "सांझा करें" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/hr.yml b/config/locales/crowdin/hr.yml index 848975d5f968..d01096755378 100644 --- a/config/locales/crowdin/hr.yml +++ b/config/locales/crowdin/hr.yml @@ -523,6 +523,10 @@ hr: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Masovno premještanje/kopiranje ne podržava radne pakete iz više projekata" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -922,6 +926,10 @@ hr: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2631,6 +2639,7 @@ hr: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "Projekt nije izbrisan." + notice_project_not_found: "Project not found." notice_successful_connection: "Uspješna spojen." notice_successful_create: "Uspješno kreirano." notice_successful_delete: "Brisanje uspješno." @@ -3395,54 +3404,56 @@ hr: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Role" - type: "Tip" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Komentar" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Uredi" - edit_description: "Can view, comment and edit this work package." - view: "Pregled" - view_description: "Can view this work package." - remove: "Ukloni" - share: "Podijeli" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Nije dijeljeno" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/hu.yml b/config/locales/crowdin/hu.yml index 9ef16bcfbda3..a4a8eaa705b0 100644 --- a/config/locales/crowdin/hu.yml +++ b/config/locales/crowdin/hu.yml @@ -513,6 +513,10 @@ hu: move: no_common_statuses_exists: "Az összes kiválasztott feladatcsoporthoz nincs elérhető állapot. Státuszuk nem változtatható meg." unsupported_for_multiple_projects: "Tömeges mozgatás/másolás nem támogatott a különböző projektekből származó feladatcsoportokhoz" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -912,6 +916,10 @@ hu: enabled_modules: dependency_missing: "A '%{dependency}' modult is engedélyezni kell, mivel a '%{module}' modul függ tőle." format: "%{message}\n" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2593,6 +2601,7 @@ hu: notice_principals_found_multiple: "%{number} találat. \n Tab-al tudsz az első találatra fókuszálni." notice_principals_found_single: "Egyetlen találat van.\nA Tab billentyű megnyomásával ugrik rá a fókusz." notice_project_not_deleted: "A projekt nem lett törölve." + notice_project_not_found: "Project not found." notice_successful_connection: "Sikeresen létrejött a kapcsolat." notice_successful_create: "Sikeres létrehozás." notice_successful_delete: "Sikeres törlés." @@ -3355,54 +3364,56 @@ hu: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Szerepkör" - type: "Típus" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Vélemény" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Szerkesztés" - edit_description: "Can view, comment and edit this work package." - view: "Nézet" - view_description: "Can view this work package." - remove: "Eltávolítás" - share: "Megosztás" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Nem megosztott" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/id.yml b/config/locales/crowdin/id.yml index 257dea077b88..5f8a6175d92c 100644 --- a/config/locales/crowdin/id.yml +++ b/config/locales/crowdin/id.yml @@ -502,6 +502,10 @@ id: move: no_common_statuses_exists: "Tidak ada status yang tersedia untuk semua paket pekerjaan yang dipilih. Status mereka tidak dapat diubah." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -901,6 +905,10 @@ id: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2556,6 +2564,7 @@ id: notice_principals_found_multiple: "Ada beberapa %{number} hasil yang ditemukan. \n Tab untuk fokus ke hasil pertama." notice_principals_found_single: "Ada satu hasil.\n Tab untuk lebih fokus." notice_project_not_deleted: "Project tidak dihapus." + notice_project_not_found: "Project not found." notice_successful_connection: "Sambungan berhasil." notice_successful_create: "Berhasil dibuat." notice_successful_delete: "Berhasil dihapus." @@ -3312,54 +3321,56 @@ id: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Role" - type: "Tipe" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Komentar" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Edit" - edit_description: "Can view, comment and edit this work package." - view: "Lihat" - view_description: "Can view this work package." - remove: "Remove" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Tidak dishare" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/it.yml b/config/locales/crowdin/it.yml index 01b152c8e80f..c0507e288071 100644 --- a/config/locales/crowdin/it.yml +++ b/config/locales/crowdin/it.yml @@ -513,6 +513,10 @@ it: move: no_common_statuses_exists: "Nessuno stato disponibile per tutte le macro-attività selezionate. Non è possibile cambiare lo stato." unsupported_for_multiple_projects: "Il comando sposta/copia multiplo non è supportato per macroattività da più progetti" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Flusso di lavoro mancante per la condivisione di macro-attività" @@ -912,6 +916,10 @@ it: enabled_modules: dependency_missing: "Anche il modulo '%{dependency}' deve essere abilitato dal momento che il modulo '%{module}' dipende da esso." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2594,6 +2602,7 @@ it: notice_principals_found_multiple: "Trovati %{number} risultati. \n Premi Tab per evidenziare il primo." notice_principals_found_single: "Trovato un solo risultato. \n Premi Tab per evidenziarlo." notice_project_not_deleted: "Il progetto non è stato eliminato." + notice_project_not_found: "Project not found." notice_successful_connection: "Connesso con successo." notice_successful_create: "Creato con successo." notice_successful_delete: "Cancellato con successo." @@ -3356,54 +3365,56 @@ it: work_based_help_text: "La % completamento viene ricavata automaticamente dal lavoro e dal lavoro residuo." status_based_help_text: "La % completamento è stabilita dallo stato della macro-attività." migration_warning_text: "Nella modalità di calcolo basata sul lavoro, la % completamento non può essere impostata manualmente ed è legata al lavoro. Il valore esistente è stato mantenuto ma non può essere modificato. Specifica prima il lavoro." - sharing: - count: - zero: "0 utenti" - one: "1 utente" - other: "%{count} utenti" - filter: - project_member: "Membro del progetto" - not_project_member: "Non è membro del progetto" - project_group: "Gruppo di progetto" - not_project_group: "Non è gruppo di progetto" - role: "Ruolo" - type: "Tipo" - label_search: "Cerca gli utenti da invitare" - label_search_placeholder: "Cerca per utente o indirizzo e-mail" - label_toggle_all: "Attiva/disattiva tutte le condivisioni" - permissions: - comment: "Commentare" - comment_description: "Può visualizzare e commentare questa macro-attività." - denied: "Non hai i permessi per condividere macro-attività." - edit: "Modificare" - edit_description: "Può visualizzare, commentare e modificare questa macro-attività." - view: "Vedere" - view_description: "Può visualizzare questa macro-attività." - remove: "Rimuovi" - share: "Condividi" - text_empty_search_description: "Non ci sono utenti con i criteri di filtro attuali." - text_empty_search_header: "Non siamo riusciti a trovare alcun risultato corrispondente." - text_empty_state_description: "La macro-attività non è stata ancora condivisa con nessuno." - text_empty_state_header: "Non condiviso" - text_user_limit_reached: "L'aggiunta di ulteriori utenti supererà il limite attuale. Contatta un amministratore per aumentare il limite di utenti e garantire che gli utenti esterni possano accedere a questa macro-attività." - text_user_limit_reached_admins: 'L''aggiunta di ulteriori utenti supererà il limite attuale. Aggiorna il tuo piano per aggiungere più utenti.' - warning_user_limit_reached: > - L'aggiunta di ulteriori utenti supererà il limite attuale. Contatta un amministratore per aumentare il limite di utenti e garantire che gli utenti esterni possano accedere a questa macro-attività. - warning_user_limit_reached_admin: > - L'aggiunta di ulteriori utenti supererà il limite attuale. Aggiorna il tuo piano per poter garantire che gli utenti esterni possano accedere a questa macro-attività. - warning_no_selected_user: "Seleziona gli utenti con cui condividere questa macro-attività" - warning_locked_user: "L'utente %{user} è bloccato e non può essere condiviso con" - user_details: - locked: "Utente bloccato" - invited: "Invito inviato." - resend_invite: "Invia di nuovo." - invite_resent: "L'invito è stato inviato di nuovo" - not_project_member: "Non è un membro del progetto" - project_group: "I membri del gruppo potrebbero avere privilegi aggiuntivi (in quanto membri del progetto)" - not_project_group: "Gruppo (condiviso con tutti i membri)" - additional_privileges_project: "Potrebbe avere privilegi aggiuntivi (in quanto membro del progetto)" - additional_privileges_group: "Potrebbe avere privilegi aggiuntivi (in quanto membro del gruppo)" - additional_privileges_project_or_group: "Potrebbe avere privilegi aggiuntivi (in quanto membro del progetto o del gruppo)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > I giorni non selezionati vengono saltati durante la pianificazione delle macro-attività (e non inclusi nel conteggio dei giorni). Questi possono essere sovrascritti a livello di macro-attività. diff --git a/config/locales/crowdin/ja.yml b/config/locales/crowdin/ja.yml index 9d10f4aab0cf..4a2f93e8050d 100644 --- a/config/locales/crowdin/ja.yml +++ b/config/locales/crowdin/ja.yml @@ -505,6 +505,10 @@ ja: move: no_common_statuses_exists: "選択されたすべてのワークパッケージに利用できるステータスはありません。 それらの状態は変更できません。" unsupported_for_multiple_projects: "複数のプロジェクトからのワークパッケージの一括移動 / コピーはサポートされていません" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -904,6 +908,10 @@ ja: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2559,6 +2567,7 @@ ja: notice_principals_found_multiple: "%{number} の検索結果があります。 \n タブで最初の結果へフォーカスします。" notice_principals_found_single: "1 つの結果があります。 \n タブでフォーカスします。" notice_project_not_deleted: "プロジェクトを削除していません。" + notice_project_not_found: "Project not found." notice_successful_connection: "正常に接続しました。" notice_successful_create: "正常に作成しました。" notice_successful_delete: "正常に削除しました。" @@ -3318,54 +3327,56 @@ ja: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "ロール" - type: "タイプ" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "コメント" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "編集" - edit_description: "Can view, comment and edit this work package." - view: "表示" - view_description: "Can view this work package." - remove: "削除" - share: "共有" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "共有しない" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/js-af.yml b/config/locales/crowdin/js-af.yml index 9056a8c5f61d..945c6c07a242 100644 --- a/config/locales/crowdin/js-af.yml +++ b/config/locales/crowdin/js-af.yml @@ -150,6 +150,7 @@ af: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ af: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ af: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-ar.yml b/config/locales/crowdin/js-ar.yml index bb17b2f740f1..7417632a7558 100644 --- a/config/locales/crowdin/js-ar.yml +++ b/config/locales/crowdin/js-ar.yml @@ -150,6 +150,7 @@ ar: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -908,6 +909,11 @@ ar: preformatted_text: "نص مُنسّقٌ مسبقًا" wiki_link: "قُم بوصله إلى صفحة Wiki" image: "صورة" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1063,12 +1069,8 @@ ar: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "شارك" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-az.yml b/config/locales/crowdin/js-az.yml index e0bdf93e1d65..6d920b38de97 100644 --- a/config/locales/crowdin/js-az.yml +++ b/config/locales/crowdin/js-az.yml @@ -150,6 +150,7 @@ az: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ az: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ az: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Paylaş" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-be.yml b/config/locales/crowdin/js-be.yml index 23e7f490bb06..1c64c69ad190 100644 --- a/config/locales/crowdin/js-be.yml +++ b/config/locales/crowdin/js-be.yml @@ -150,6 +150,7 @@ be: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -906,6 +907,11 @@ be: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1061,12 +1067,8 @@ be: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-bg.yml b/config/locales/crowdin/js-bg.yml index 0621a5de2928..ac03b349cc13 100644 --- a/config/locales/crowdin/js-bg.yml +++ b/config/locales/crowdin/js-bg.yml @@ -150,6 +150,7 @@ bg: attribute_reference: macro_help_tooltip: "Този текстов сегмент се визуализира динамично от макрос." not_found: "Заявеният ресурс не можа да бъде намерен" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "Избраният атрибут „%{name}“ не съществува." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ bg: preformatted_text: "Предварително форматиран текст" wiki_link: "Линк към уики страница" image: "Изображение" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Групова промяна на проекта" @@ -1059,12 +1065,8 @@ bg: is_parent: "Датите на този работен пакет се извеждат автоматично от неговите деца. Активирайте „Ръчно планиране“, за да зададете датите." is_switched_from_manual_to_automatic: "Датите на този работен пакет може да се наложи да бъдат преизчислени след преминаване от ръчно към автоматично планиране поради връзки с други работни пакети." sharing: - share: "Споделяне" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-ca.yml b/config/locales/crowdin/js-ca.yml index 5b8f6f7aad2b..cd90bfce5de1 100644 --- a/config/locales/crowdin/js-ca.yml +++ b/config/locales/crowdin/js-ca.yml @@ -150,6 +150,7 @@ ca: attribute_reference: macro_help_tooltip: "Aquest segment de text s'està presentant dinàmicament per una macro." not_found: "El recurs sol·licitat no s'ha trobat" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "L'atribut seleccionat \"%{name}\" no existeix." child_pages: button: "Enllaços a pàgines filles" @@ -904,6 +905,11 @@ ca: preformatted_text: "Text preformatat" wiki_link: "Enllaça a una pàgina Wiki" image: "Imatge" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Canvi massiu de projecte" @@ -1059,12 +1065,8 @@ ca: is_parent: "Les dates d'aquest paquet de treball estan derivades automàticament dels seus fills. Activa la \"Planificació manual\" per canviar les dates." is_switched_from_manual_to_automatic: "Pot ser que les dates d'aquest paquet de treball no es tornin a calcular automàticament en canviar de planificació manual a automàtica a causa de les relacions amb altres paquets de treball." sharing: - share: "Compartir" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-ckb-IR.yml b/config/locales/crowdin/js-ckb-IR.yml index 197758f70aff..ae92eb54e65f 100644 --- a/config/locales/crowdin/js-ckb-IR.yml +++ b/config/locales/crowdin/js-ckb-IR.yml @@ -150,6 +150,7 @@ ckb-IR: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ ckb-IR: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ ckb-IR: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-cs.yml b/config/locales/crowdin/js-cs.yml index 9f3313ee5c5b..b62a84462920 100644 --- a/config/locales/crowdin/js-cs.yml +++ b/config/locales/crowdin/js-cs.yml @@ -150,6 +150,7 @@ cs: attribute_reference: macro_help_tooltip: "Tento textový segment je dynamicky rendrován makrem." not_found: "Požadovaný zdroj nebyl nalezen." + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "Vybraný atribut '%{name}' neexistuje." child_pages: button: "Odkazy na podřízené stránky" @@ -905,6 +906,11 @@ cs: preformatted_text: "Předformátovaný Text" wiki_link: "Odkaz na stránku Wiki" image: "Obrázek" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Hromadná změna projektu" @@ -1060,12 +1066,8 @@ cs: is_parent: "Data tohoto pracovního balíčku jsou automaticky odvozena od jeho dětí. Aktivujte 'Manuální plánování' pro ručné nastavení dat." is_switched_from_manual_to_automatic: "Data tohoto pracovního balíčku budou možná muset být přepočítána po přepnutí z manuálu na automatické plánování kvůli vztahům s ostatními pracovními balíčky." sharing: - share: "Sdílet" title: "Sdílet pracovní balíček" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} vybráno" - selection: - mixed: "Smíšená" upsale: description: "Sdílejte pracovní balíčky s uživateli, kteří nejsou členy projektu." table: diff --git a/config/locales/crowdin/js-da.yml b/config/locales/crowdin/js-da.yml index bc75d5487442..b6d874a78aa4 100644 --- a/config/locales/crowdin/js-da.yml +++ b/config/locales/crowdin/js-da.yml @@ -150,6 +150,7 @@ da: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links til undersider" @@ -903,6 +904,11 @@ da: preformatted_text: "Forudformatteret tekst" wiki_link: "Link til wiki-side" image: "Billede" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1058,12 +1064,8 @@ da: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Del" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-de.yml b/config/locales/crowdin/js-de.yml index 5f9d0b62c93f..5654a2c31d23 100644 --- a/config/locales/crowdin/js-de.yml +++ b/config/locales/crowdin/js-de.yml @@ -150,6 +150,7 @@ de: attribute_reference: macro_help_tooltip: "Dieser Textabschnitt wird dynamisch durch ein Makro erzeugt." not_found: "Die angeforderte Ressource konnte nicht gefunden werden" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "Das ausgewählte Attribut '%{name}' existiert nicht." child_pages: button: "Auflistung untergeordneter Seiten" @@ -903,6 +904,11 @@ de: preformatted_text: "Präformatierter Text" wiki_link: "Link zu einer Wikiseite" image: "Bild" + sharing: + share: "Teilen" + selected_count: "%{count} ausgewählt" + selection: + mixed: "Gemischt" work_packages: bulk_actions: move: "In anderes Projekt verschieben" @@ -1058,12 +1064,8 @@ de: is_parent: "Die Termine dieses Arbeitspakets werden automatisch von seinen Kindern aggregiert. Aktivieren Sie \"Manuelle Planung\", um die Termine frei zu setzen." is_switched_from_manual_to_automatic: "Die Termine dieses Arbeitspakets müssen nach dem Wechsel auf automatische Planung neu berechnet werden." sharing: - share: "Teilen" title: "Arbeitspaket teilen" show_all_users: "Alle Benutzer anzeigen, für die das Arbeitspaket geteilt wurde" - selected_count: "%{count} ausgewählt" - selection: - mixed: "Gemischt" upsale: description: "Teilen Sie Arbeitspakete mit Benutzern, die nicht Mitglieder des Projekts sind." table: diff --git a/config/locales/crowdin/js-el.yml b/config/locales/crowdin/js-el.yml index 924e744f797e..13b2d127b192 100644 --- a/config/locales/crowdin/js-el.yml +++ b/config/locales/crowdin/js-el.yml @@ -150,6 +150,7 @@ el: attribute_reference: macro_help_tooltip: "Αυτό το τμήμα κειμένου δημιουργείται δυναμικά από μια μακροεντολή." not_found: "Ο ζητούμενος πόρος δεν βρέθηκε." + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "Το επιλεγμένο χαρακτηριστικό '%{name}' δεν υπάρχει." child_pages: button: "Σύνδεσμοι σε σελίδες-παιδιά" @@ -903,6 +904,11 @@ el: preformatted_text: "Προδιαμορφωμένο κείμενο" wiki_link: "Σύνδεση σε μια σελίδα Wiki" image: "Εικόνα" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Μαζική αλλαγή του έργου" @@ -1058,12 +1064,8 @@ el: is_parent: "Οι ημερομηνίες αυτού του πακέτου εργασίας προκύπτουν αυτόματα από τα παιδιά του. Ενεργοποιήστε τον \"Χειροκίνητο προγραμματισμό\" για να ορίσετε τις ημερομηνίες." is_switched_from_manual_to_automatic: "Οι ημερομηνίες αυτού του πακέτου εργασίας μπορεί να χρειαστεί να υπολογιστούν εκ νέου μετά τη μετάβαση από χειροκίνητο σε αυτόματο προγραμματισμό λόγω των σχέσεων με άλλα πακέτα εργασίας." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-eo.yml b/config/locales/crowdin/js-eo.yml index 34160bf04091..335de81eaaba 100644 --- a/config/locales/crowdin/js-eo.yml +++ b/config/locales/crowdin/js-eo.yml @@ -150,6 +150,7 @@ eo: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ eo: preformatted_text: "Preformatted Text" wiki_link: "Ligi al vikipaĝo" image: "Bildo" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ eo: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-es.yml b/config/locales/crowdin/js-es.yml index a687c3c3b219..12800b785aa1 100644 --- a/config/locales/crowdin/js-es.yml +++ b/config/locales/crowdin/js-es.yml @@ -150,6 +150,7 @@ es: attribute_reference: macro_help_tooltip: "Este segmento de texto está siendo renderizado dinámicamente por una macro." not_found: "No se encuentra el recurso solicitado" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "El atributo seleccionado («%{name}») no existe." child_pages: button: "Enlaces a páginas secundarias" @@ -904,6 +905,11 @@ es: preformatted_text: "Texto Preformateado" wiki_link: "Vincular a una página Wiki" image: "Imagen" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Cambio masivo del proyecto" @@ -1059,12 +1065,8 @@ es: is_parent: "Las fechas de este paquete de trabajo son deducidas automáticamente de sus sub-elementos. Active 'Programación manual' para establecer las fechas." is_switched_from_manual_to_automatic: "Las fechas de este paquete de trabajo pueden necesitar ser recalculadas después de pasar de programación manual a programación automática debido a las relaciones con otros paquetes de trabajo." sharing: - share: "Compartir" title: "Compartir paquete de trabajo" show_all_users: "Mostrar todos los usuarios con los que se ha compartido el paquete de trabajo" - selected_count: "%{count} seleccionados" - selection: - mixed: "Mixto" upsale: description: "Compartir paquetes de trabajo con usuarios que no son miembros del proyecto." table: diff --git a/config/locales/crowdin/js-et.yml b/config/locales/crowdin/js-et.yml index cdf9588a3a6b..040645123cea 100644 --- a/config/locales/crowdin/js-et.yml +++ b/config/locales/crowdin/js-et.yml @@ -150,6 +150,7 @@ et: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ et: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Pilt" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ et: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Jaga" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-eu.yml b/config/locales/crowdin/js-eu.yml index bc8f28456194..8038b0d8b4ed 100644 --- a/config/locales/crowdin/js-eu.yml +++ b/config/locales/crowdin/js-eu.yml @@ -150,6 +150,7 @@ eu: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ eu: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ eu: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-fa.yml b/config/locales/crowdin/js-fa.yml index 6c19ee987e32..255951b5178b 100644 --- a/config/locales/crowdin/js-fa.yml +++ b/config/locales/crowdin/js-fa.yml @@ -150,6 +150,7 @@ fa: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ fa: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ fa: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "به اشتراک گذاری" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-fi.yml b/config/locales/crowdin/js-fi.yml index 2e3dc3a69bbc..3989047d8b41 100644 --- a/config/locales/crowdin/js-fi.yml +++ b/config/locales/crowdin/js-fi.yml @@ -150,6 +150,7 @@ fi: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Linkit alasivuihin" @@ -904,6 +905,11 @@ fi: preformatted_text: "Muotoiltu teksti" wiki_link: "Linkitys Wiki-sivulle" image: "Kuva" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ fi: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Jaa" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-fil.yml b/config/locales/crowdin/js-fil.yml index 2ef42a71c2bd..44e0a916b17b 100644 --- a/config/locales/crowdin/js-fil.yml +++ b/config/locales/crowdin/js-fil.yml @@ -150,6 +150,7 @@ fil: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ fil: preformatted_text: "Tekstong naka-preformat" wiki_link: "Ang link sa pahinang wiki" image: "Larawan" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ fil: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Ibahagi" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-fr.yml b/config/locales/crowdin/js-fr.yml index 074c59e99972..2962408ea67d 100644 --- a/config/locales/crowdin/js-fr.yml +++ b/config/locales/crowdin/js-fr.yml @@ -150,6 +150,7 @@ fr: attribute_reference: macro_help_tooltip: "Ce segment de texte est rendu dynamiquement par une macro." not_found: "La ressource demandée est introuvable" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "L'attribut « %{name} » n'existe pas." child_pages: button: "Liens vers les pages enfants" @@ -904,6 +905,11 @@ fr: preformatted_text: "Texte préformaté" wiki_link: "Lien vers une page wiki" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Changement de projet en bloc" @@ -1059,12 +1065,8 @@ fr: is_parent: "Les dates de ce lot de travaux sont automatiquement déduites de ses enfants. Activez la « Planification manuelle » pour définir les dates." is_switched_from_manual_to_automatic: "Les dates de ce lot de travaux peuvent avoir besoin d'être recalculées après avoir passé de la planification manuelle à la planification automatique en raison de relations avec d'autres lots de travaux." sharing: - share: "Partager" title: "Partager le lot de travaux" show_all_users: "Afficher tous les utilisateurs avec lesquels le lot de travaux a été partagé" - selected_count: "%{count} sélectionné" - selection: - mixed: "Mixte" upsale: description: "Partagez les lots de travaux avec des utilisateurs qui ne sont pas membres du projet." table: diff --git a/config/locales/crowdin/js-he.yml b/config/locales/crowdin/js-he.yml index f6875b3af8c6..749e769adfd6 100644 --- a/config/locales/crowdin/js-he.yml +++ b/config/locales/crowdin/js-he.yml @@ -150,6 +150,7 @@ he: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -906,6 +907,11 @@ he: preformatted_text: "טקסט מעוצב מראש" wiki_link: "קישור לדף Wiki" image: "תמונה" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1061,12 +1067,8 @@ he: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "שתף" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-hi.yml b/config/locales/crowdin/js-hi.yml index 059fa7fca119..adcc04f4b8c7 100644 --- a/config/locales/crowdin/js-hi.yml +++ b/config/locales/crowdin/js-hi.yml @@ -150,6 +150,7 @@ hi: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ hi: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ hi: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "सांझा करें" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-hr.yml b/config/locales/crowdin/js-hr.yml index c012c375e3ac..95ad35891432 100644 --- a/config/locales/crowdin/js-hr.yml +++ b/config/locales/crowdin/js-hr.yml @@ -150,6 +150,7 @@ hr: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -905,6 +906,11 @@ hr: preformatted_text: "Preformatirani tekst" wiki_link: "Poveznica na Wiki stranicu" image: "Fotografija" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1060,12 +1066,8 @@ hr: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Podijeli" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-hu.yml b/config/locales/crowdin/js-hu.yml index 4629249a54d0..55b53ed00d1b 100644 --- a/config/locales/crowdin/js-hu.yml +++ b/config/locales/crowdin/js-hu.yml @@ -150,6 +150,7 @@ hu: attribute_reference: macro_help_tooltip: "Ezt a szövegrészt dinamikusan megjeleníti egy makró." not_found: "A kért erőforrás nem található." + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "A kiválasztott \"%{name}\" attribútum nem létezik." child_pages: button: "Gyermek kapcsolatok" @@ -904,6 +905,11 @@ hu: preformatted_text: "Előre formázott szöveg" wiki_link: "Linket egy Wiki oldalhoz" image: "Kép" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "A projekt tömeges megváltoztatása" @@ -1059,12 +1065,8 @@ hu: is_parent: "A munkacsomag dátumait automatikusan levezetik a gyermekeiből. A dátumok beállításához aktiválja a „Kézi ütemezést”" is_switched_from_manual_to_automatic: "Ennek a munkacsomagnak a dátumait újra kell kalkulálni, miután a kézi üzemmódról az automatikus ütemezésre váltott, más munkacsomagokkal való kapcsolat miatt.\n" sharing: - share: "Megosztás" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-id.yml b/config/locales/crowdin/js-id.yml index 273cf6959646..9d2e26ca1cff 100644 --- a/config/locales/crowdin/js-id.yml +++ b/config/locales/crowdin/js-id.yml @@ -150,6 +150,7 @@ id: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Tautkan ke anak halaman" @@ -903,6 +904,11 @@ id: preformatted_text: "Preformatted Text" wiki_link: "Link ke halaman Wiki" image: "Gambar" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1058,12 +1064,8 @@ id: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-it.yml b/config/locales/crowdin/js-it.yml index cadb5f6eef96..c989cb6d2ba0 100644 --- a/config/locales/crowdin/js-it.yml +++ b/config/locales/crowdin/js-it.yml @@ -150,6 +150,7 @@ it: attribute_reference: macro_help_tooltip: "Questo segmento di testo viene renderizzato dinamicamente da una macro." not_found: "Impossibile trovare la risorsa richiesta" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "L'attributo selezionato '%{name}' non esiste." child_pages: button: "Collegamenti alle pagine discendenti" @@ -904,6 +905,11 @@ it: preformatted_text: "Testo preformattato" wiki_link: "Collega ad una pagina Wiki" image: "Immagine" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Modifica complessiva del progetto" @@ -1059,12 +1065,8 @@ it: is_parent: "Le date di questa macro-attività vengono automaticamente dedotte dai suoi figli. Attiva 'Programmazione manuale' per impostare le date." is_switched_from_manual_to_automatic: "Potrebbe essere necessario ricalcolare le date di questa macro-attività dopo il passaggio dalla programmazione manuale alla programmazione automatica a causa delle relazioni con altre macro-attività." sharing: - share: "Condividi" title: "Condividi macro-attività" show_all_users: "Mostra tutti gli utenti con i quali la macro-attività è stata condivisa" - selected_count: "%{count} selezionati" - selection: - mixed: "Misto" upsale: description: "Condividi macro-attività con utenti che non sono membri del progetto." table: diff --git a/config/locales/crowdin/js-ja.yml b/config/locales/crowdin/js-ja.yml index d789765e09d0..7c89e8d07ba7 100644 --- a/config/locales/crowdin/js-ja.yml +++ b/config/locales/crowdin/js-ja.yml @@ -151,6 +151,7 @@ ja: attribute_reference: macro_help_tooltip: "このテキストセグメントはマクロによって動的にレンダリングされています。" not_found: "要求されたリソースが見つかりませんでした" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "選択した属性 '%{name}' は存在しません。" child_pages: button: "子ページへのリンク" @@ -904,6 +905,11 @@ ja: preformatted_text: "整形済みテキスト" wiki_link: "Wikiページにリンクする" image: "画像" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "プロジェクトの一括変更" @@ -1059,12 +1065,8 @@ ja: is_parent: "このワークパッケージの日付は、その子から自動的に推定されます。日付を設定するには、「手動スケジューリング」を有効にします。" is_switched_from_manual_to_automatic: "他のワークパッケージとの関連により、手動スケジューリングから自動スケジューリングに切り替えた後に、このワークパッケージの日付を再計算する必要がある場合があります。" sharing: - share: "共有" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-ka.yml b/config/locales/crowdin/js-ka.yml index a8cdd331e521..75ab1dca862d 100644 --- a/config/locales/crowdin/js-ka.yml +++ b/config/locales/crowdin/js-ka.yml @@ -150,6 +150,7 @@ ka: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ ka: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "გამოსახულება" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ ka: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "გაზიარება" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "შერეული" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-kk.yml b/config/locales/crowdin/js-kk.yml index bbb4ad61f652..21f527512724 100644 --- a/config/locales/crowdin/js-kk.yml +++ b/config/locales/crowdin/js-kk.yml @@ -150,6 +150,7 @@ kk: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ kk: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ kk: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-ko.yml b/config/locales/crowdin/js-ko.yml index f9b64d6ce1d3..0fd4937a9411 100644 --- a/config/locales/crowdin/js-ko.yml +++ b/config/locales/crowdin/js-ko.yml @@ -150,6 +150,7 @@ ko: attribute_reference: macro_help_tooltip: "이 텍스트 세그먼트는 매크로에 의해 동적으로 렌더링되고 있습니다." not_found: "요청한 리소스를 찾을 수 없습니다." + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "선택한 특성 '%{name}'이(가) 없습니다." child_pages: button: "자식 페이지의 링크" @@ -903,6 +904,11 @@ ko: preformatted_text: "서식 설정되어 있는 텍스트" wiki_link: "위키 페이지에 연결" image: "이미지" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "프로젝트의 일괄 변경" @@ -1058,12 +1064,8 @@ ko: is_parent: "이 작업 패키지의 날짜는 자식으로부터 자동으로 추정됩니다. '수동 스케줄링'을 활성화하여 날짜를 설정할 수 있습니다." is_switched_from_manual_to_automatic: "다른 작업 패키지와의 관계로 인해 수동 스케줄링에서 자동 스케줄링으로 전환한 후 이 작업 패키지의 날짜를 다시 계산해야 할 수 있습니다." sharing: - share: "공유하기" title: "작업 패키지 공유" show_all_users: "작업 패키지가 공유된 모든 사용자 표시" - selected_count: "%{count}개 선택됨" - selection: - mixed: "혼합" upsale: description: "프로젝트 멤버가 아닌 사용자와 작업 패키지를 공유합니다." table: diff --git a/config/locales/crowdin/js-lt.yml b/config/locales/crowdin/js-lt.yml index 5838ddd6de2a..8d83fe7e1bfe 100644 --- a/config/locales/crowdin/js-lt.yml +++ b/config/locales/crowdin/js-lt.yml @@ -150,6 +150,7 @@ lt: attribute_reference: macro_help_tooltip: "Šis teksto segmentas yra dinamiškai sukuriamas makrokomandos." not_found: "Reikalaujamas resursas nerastas" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "Pasirinktas atributas „%{name}“ neegzistuoja." child_pages: button: "Nuorodos į vaikų puslapius" @@ -906,6 +907,11 @@ lt: preformatted_text: "Iš anksto suformatuotas tekstas" wiki_link: "Nuoroda į Wiki puslapį" image: "Paveikslėlis" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Masinis projekto pakeitimas" @@ -1061,12 +1067,8 @@ lt: is_parent: "Šio darbų paketo datos yra automatiškai nustatomos pagal jo vaikų datas. Norėdami nustatyti kitokias datas įjunkite „Rankinį tvarkaraščio parinkimą“" is_switched_from_manual_to_automatic: "Po perjungimo iš rankinio į automatinį tvarkaraščio parinkimą, šio darbų paketo datas gali reikėti perskaičiuoti, nes yra sąryšių su kitais darbų paketais." sharing: - share: "Dalintis" title: "Dalintis darbo paketu" show_all_users: "Rodyti visus naudotojus, su kuriais bendrinamas šis darbo paketas" - selected_count: "%{count} pažymėta" - selection: - mixed: "Mišrus" upsale: description: "Bendrinkite darbo paketus su naudotojais, kurie nėra projekto nariai." table: diff --git a/config/locales/crowdin/js-lv.yml b/config/locales/crowdin/js-lv.yml index 423982cba02d..77a250881c7e 100644 --- a/config/locales/crowdin/js-lv.yml +++ b/config/locales/crowdin/js-lv.yml @@ -150,6 +150,7 @@ lv: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -905,6 +906,11 @@ lv: preformatted_text: "Pre" wiki_link: "Saite uz Wiki lapu" image: "Attēls" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1060,12 +1066,8 @@ lv: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Koplietot" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-mn.yml b/config/locales/crowdin/js-mn.yml index 65156ac0e819..f376b23e167d 100644 --- a/config/locales/crowdin/js-mn.yml +++ b/config/locales/crowdin/js-mn.yml @@ -150,6 +150,7 @@ mn: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ mn: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ mn: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-ms.yml b/config/locales/crowdin/js-ms.yml index 70e95b9354c1..446e1e502f48 100644 --- a/config/locales/crowdin/js-ms.yml +++ b/config/locales/crowdin/js-ms.yml @@ -150,6 +150,7 @@ ms: attribute_reference: macro_help_tooltip: "Bahagian teks ini sedang dihasilkan secara dinamik oleh makro." not_found: "Sumber yang diminta tidak dapat ditemui" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "Atribut yang dipilih '%{name}' tidak wujud." child_pages: button: "Terpaut pada halaman anak" @@ -368,7 +369,7 @@ ms: "14_2": standard: new_features_html: > - The release contains various new features and improvements:
    • Set more units for work, remaining work and working time
    • Configure which projects are activated for project attributes
    • Rename private project lists
    • Avoid redundant email reminders in case of @mentions
    • Invite meeting attendees via email
    • Display embedded work package attributes in PDF exports
    + Pelancaran tersebut mengandungi pelbagai fitur dan penambahbaikan baharu:
      • Tetapkan lebih banyak unit pada kerja, kerja yang berbaki dan masa yang berbaki
      • Konfigurasikan projek yang manakah yang telah diaktifkan bagi atribut projek
      • Namakan semula senarai projek peribadi
      • Elakkan daripada peringatan e-mel yang tidak diperlukan sekiranya @sebutan
      • Jemput penghadir mesyuarat melalui e-mel
      • Paparkan atribut pakej kerja yang tersemat di eksport PDF
      ical_sharing_modal: title: "Langgan kalendar" inital_setup_error_message: "Ralat berlaku ketika sedang mengambil data." @@ -903,6 +904,11 @@ ms: preformatted_text: "Teks yang telah dipraformatkan" wiki_link: "Pautan ke halaman Wiki" image: "Imej" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Perubahan projek secara pukal" @@ -1058,12 +1064,8 @@ ms: is_parent: "Tarikh pakej kerja ini disimpulkan secara automatik dari anaknya. Aktifkan 'Penjadualan Manual' untuk tetapkan tarikh." is_switched_from_manual_to_automatic: "Tarikh pakej kerja ini mungkin perlu dikira semula selepas menukar dari penjadualan manual ke automatik kerana hubungan dengan pakej kerja lain." sharing: - share: "Kongsi" title: "Kongsi pakej kerja" show_all_users: "Paparkan kepada semua pengguna dengan siapa pakej kerja telah dikongsikan" - selected_count: "%{count} dipilih" - selection: - mixed: "Bercampur" upsale: description: "Kongsi pakej kerja dengan pengguna yang bukan ahli projek." table: @@ -1357,4 +1359,4 @@ ms: network_off: "Terdapat masalah rangkaian." network_on: "Rangkaian kembali. Kami sedang mencuba." timeout: > - OpenProject could not provide you access to the project folder within the expected period of time. Please, try once again.

      If that problem persists please contact your OpenProject administrator to check the health status of the file storage setup. + OpenProject tidak dapat memberikan anda akses ke folder projek dalam tempoh masa yang dijangkakan. Sila cubasekali lagi.

      Jika masalah itu berterusan, sila hubungi pentadbir OpenProject anda untuk menyemak status kesihatan penyediaan storan fail. diff --git a/config/locales/crowdin/js-ne.yml b/config/locales/crowdin/js-ne.yml index 1910de09d3e9..3943ae5dca8b 100644 --- a/config/locales/crowdin/js-ne.yml +++ b/config/locales/crowdin/js-ne.yml @@ -150,6 +150,7 @@ ne: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ ne: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ ne: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-nl.yml b/config/locales/crowdin/js-nl.yml index 9bf96d8cf841..dfc8102ce419 100644 --- a/config/locales/crowdin/js-nl.yml +++ b/config/locales/crowdin/js-nl.yml @@ -150,6 +150,7 @@ nl: attribute_reference: macro_help_tooltip: "Dit tekstsegment wordt dynamisch weergegeven door een macro." not_found: "Gevraagde bron kan niet worden gevonden" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "Het geselecteerde attribuut '%{name}' bestaat niet." child_pages: button: "Links naar onderliggende pagina 's" @@ -904,6 +905,11 @@ nl: preformatted_text: "Voorgeformateerde Tekst" wiki_link: "Link naar een Wiki-pagina" image: "Afbeelding" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk verandering van project" @@ -1059,12 +1065,8 @@ nl: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Deel" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-no.yml b/config/locales/crowdin/js-no.yml index c0e412c03dbb..70f8f3303685 100644 --- a/config/locales/crowdin/js-no.yml +++ b/config/locales/crowdin/js-no.yml @@ -150,6 +150,7 @@ attribute_reference: macro_help_tooltip: "Dette tekstsegmentet er dynamisk gjengitt av en makro." not_found: "Finner ikke forespurt ressurs" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "Den valgte egenskapen '%{name}' eksisterer ikke." child_pages: button: "Lenker til undersider" @@ -904,6 +905,11 @@ preformatted_text: "Preformattert tekst" wiki_link: "Lenke til en Wiki-side" image: "Bilde" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Endre prosjekt i bulk" @@ -1059,12 +1065,8 @@ is_parent: "Datoene for denne arbeidspakken trekkes automatisk fra de underordnede. Aktiver 'Manuell planlegging' for å angi datoene." is_switched_from_manual_to_automatic: "Datoene for denne arbeidspakken må eventuelt beregnes på nytt etter å ha byttet fra manual til automatisk planlegging på grunn av relasjoner med andre arbeidspakker." sharing: - share: "Del" title: "Del arbeidspakke" show_all_users: "Vis alle brukere som arbeidspakken er delt med" - selected_count: "%{count} valgte" - selection: - mixed: "Blandet" upsale: description: "Del arbeidspakker med brukere som ikke er medlemmer i prosjektet." table: diff --git a/config/locales/crowdin/js-pl.yml b/config/locales/crowdin/js-pl.yml index 9bab8063ec4a..dd9d58d40f86 100644 --- a/config/locales/crowdin/js-pl.yml +++ b/config/locales/crowdin/js-pl.yml @@ -150,6 +150,7 @@ pl: attribute_reference: macro_help_tooltip: "Ten segment tekstowy jest renderowany dynamicznie przez makro." not_found: "Nie można znaleźć żądanego zasobu" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "Wybrany atrybut „%{name}” nie istnieje." child_pages: button: "Łącza do stron podrzędnych" @@ -906,6 +907,11 @@ pl: preformatted_text: "Tekst wstępnie sformatowany" wiki_link: "Adres strony Wiki" image: "Obraz" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Masowa zmiana projektu" @@ -1061,12 +1067,8 @@ pl: is_parent: "Daty tego pakietu roboczego są automatycznie odejmowane od jego elementów podrzędnych. Włącz funkcję Planowanie ręczne, aby ustawić daty." is_switched_from_manual_to_automatic: "Po przełączeniu planowania z ręcznego na automatyczne konieczne może być ponowne obliczenie dat tego pakietu roboczego ze względu na jego powiązania z innymi pakietami roboczymi." sharing: - share: "Udostępnij" title: "Udostępnij pakiet roboczy" show_all_users: "Pokaż wszystkich użytkowników, którym udostępniono pakiet roboczy" - selected_count: "Wybrano %{count}" - selection: - mixed: "Mieszane" upsale: description: "Udostępnij pakiety robocze użytkownikom, którzy nie są członkami projektu." table: diff --git a/config/locales/crowdin/js-pt-BR.yml b/config/locales/crowdin/js-pt-BR.yml index 0b4737a55dd4..9e8719b66da6 100644 --- a/config/locales/crowdin/js-pt-BR.yml +++ b/config/locales/crowdin/js-pt-BR.yml @@ -150,6 +150,7 @@ pt-BR: attribute_reference: macro_help_tooltip: "Esse segmento de texto está sendo dinamicamente renderizado por uma macro." not_found: "Não foi possível encontrar o recurso solicitado" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "O atributo selecionado '%{name}' não existe." child_pages: button: "Vínculos para páginas filhas" @@ -903,6 +904,11 @@ pt-BR: preformatted_text: "Texto pré-formatado" wiki_link: "Link para uma página Wiki" image: "Imagem" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Mudança em massa de projeto" @@ -1058,12 +1064,8 @@ pt-BR: is_parent: "As datas deste pacote de trabalho são deduzidas automaticamente de seus filhos. Ative o 'Planejamento manual' para definir as datas." is_switched_from_manual_to_automatic: "As datas deste pacote de trabalho podem precisar ser recalculadas após a alteração de planejamento manual para automático devido a relações com outros pacotes de trabalho." sharing: - share: "Compartilhar" title: "Compartilhar pacote de trabalho" show_all_users: "Exibir todos os usuários com quem o pacote de trabalho foi compartilhado" - selected_count: "%{count} selecionado" - selection: - mixed: "Misturado" upsale: description: "Compartilhe pacotes de trabalho com usuários que não são membros do projeto." table: diff --git a/config/locales/crowdin/js-pt-PT.yml b/config/locales/crowdin/js-pt-PT.yml index d4ec755f123f..5724467516a8 100644 --- a/config/locales/crowdin/js-pt-PT.yml +++ b/config/locales/crowdin/js-pt-PT.yml @@ -150,6 +150,7 @@ pt-PT: attribute_reference: macro_help_tooltip: "Este segmento de texto está a ser dinamicamente processado por uma macro." not_found: "Não foi possível encontrar o recurso solicitado" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "O atributo selecionado '%{name}' não existe." child_pages: button: "Links para páginas filho" @@ -904,6 +905,11 @@ pt-PT: preformatted_text: "Texto pré-formatado" wiki_link: "Link para uma página Wiki" image: "Imagem" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Mudança em massa de projeto" @@ -1059,12 +1065,8 @@ pt-PT: is_parent: "As datas deste pacote de trabalho são deduzidas automaticamente dos seus filhos. Ative o 'Agendamento manual' para definir as datas." is_switched_from_manual_to_automatic: "As datas deste pacote de trabalho podem precisar de ser recalculadas após mudar de agendamento manual para automático, devido a relações com outros pacotes de trabalho." sharing: - share: "Partilhe" title: "Partilhar pacote de trabalho" show_all_users: "Mostrar todos os utilizadores com quem o pacote de trabalho foi partilhado" - selected_count: "%{count} selecionado" - selection: - mixed: "Misturado" upsale: description: "Partilhe pacotes de trabalho com utilizadores que não são membros do projeto." table: diff --git a/config/locales/crowdin/js-ro.yml b/config/locales/crowdin/js-ro.yml index db3367114318..749fcc1846cf 100644 --- a/config/locales/crowdin/js-ro.yml +++ b/config/locales/crowdin/js-ro.yml @@ -150,6 +150,7 @@ ro: attribute_reference: macro_help_tooltip: "Acest segment de text este redat în mod dinamic de o macro." not_found: "Resursa solicitată nu a fost găsită." + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "Atributul selectat \"%{name}\" nu există." child_pages: button: "Legături către paginile pentru copii" @@ -904,6 +905,11 @@ ro: preformatted_text: "Text preformatat" wiki_link: "Link la o pagină Wiki" image: "Imagine" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Modificare identificator proiect" @@ -1059,12 +1065,8 @@ ro: is_parent: "Datele acestui pachet de lucru sunt deduse automat din copiii săi. Activați \"Programare manuală\" pentru a stabili datele." is_switched_from_manual_to_automatic: "Este posibil ca datele acestui pachet de lucru să trebuiască să fie recalculate după trecerea de la programarea manuală la cea automată din cauza relațiilor cu alte pachete de lucru." sharing: - share: "Distribuiți" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-ru.yml b/config/locales/crowdin/js-ru.yml index 939f1fdca5cd..7eb81adcacac 100644 --- a/config/locales/crowdin/js-ru.yml +++ b/config/locales/crowdin/js-ru.yml @@ -150,6 +150,7 @@ ru: attribute_reference: macro_help_tooltip: "Этот текстовый сегмент динамически отображается макросом." not_found: "Запрошенный ресурс не найден" + nested_macro: "Этот макрос рекурсивно ссылается на %{model} %{id}." invalid_attribute: "Выбранный атрибут '%{name}' не существует." child_pages: button: "Ссылки на дочерние страницы" @@ -307,7 +308,7 @@ ru: two: "Второй критерий сортировки" three: "Третий критерий сортировки" gantt_chart: - label: "График Гантта" + label: "Диаграмма Ганта" quarter_label: "Q%{quarter_number}" labels: title: "Конфигурация метки" @@ -317,8 +318,8 @@ ru: farRight: "Далеко справа" description: > Выберите атрибуты, которые вы хотите постоянно отображать в соответствующих позициях диаграммы Ганта. Обратите внимание, что при наведении курсора мыши на элемент вместо этих атрибутов будут отображаться метки даты. - button_activate: "Показать график Гантта" - button_deactivate: "Скрыть график Гантта" + button_activate: "Показать диаграмму Ганта" + button_deactivate: "Скрыть диаграмму Ганта" filter: noneSelection: "(нет)" selection_mode: @@ -905,6 +906,11 @@ ru: preformatted_text: "Предварительно отформатированный текст" wiki_link: "Ссылка на wiki страницу" image: "Изображение" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Массовое изменение проекта" @@ -1060,12 +1066,8 @@ ru: is_parent: "Даты этого пакета работ выводятся автоматически из дочерних пакетов работ. Используйте «Ручной планировщик», чтобы установить даты." is_switched_from_manual_to_automatic: "Даты этого пакета работ могут быть пересчитаны после переключения с ручного на автоматическое планирование из-за связей с другими пакетами работ." sharing: - share: "Поделиться" title: "Поделиться пакетом работ" show_all_users: "Показать всех пользователей, которым был предоставлен общий доступ к пакету работ" - selected_count: "%{count} выбрано" - selection: - mixed: "Смешанное" upsale: description: "Поделитесь пакетами работ с пользователями, не являющимися участниками проекта." table: diff --git a/config/locales/crowdin/js-rw.yml b/config/locales/crowdin/js-rw.yml index 751df045ff27..70949dd4966c 100644 --- a/config/locales/crowdin/js-rw.yml +++ b/config/locales/crowdin/js-rw.yml @@ -150,6 +150,7 @@ rw: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ rw: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ rw: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-si.yml b/config/locales/crowdin/js-si.yml index 46ba13af1ed7..840edbcdfd32 100644 --- a/config/locales/crowdin/js-si.yml +++ b/config/locales/crowdin/js-si.yml @@ -150,6 +150,7 @@ si: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "ළමා පිටු වලට සබැඳි" @@ -904,6 +905,11 @@ si: preformatted_text: "පෙර ආකෘතිකරණය කරන ලද පෙළ" wiki_link: "විකි පිටුවකට සබැඳිය" image: "පින්තූරය" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "ව්යාපෘතියේ තොග වෙනසක්" @@ -1059,12 +1065,8 @@ si: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-sk.yml b/config/locales/crowdin/js-sk.yml index 780233955567..172fae3e50ca 100644 --- a/config/locales/crowdin/js-sk.yml +++ b/config/locales/crowdin/js-sk.yml @@ -150,6 +150,7 @@ sk: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Odkazy na podstránky" @@ -906,6 +907,11 @@ sk: preformatted_text: "Predformátovaný Text" wiki_link: "Odkaz na Wiki stránku" image: "Obrázok" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1061,12 +1067,8 @@ sk: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Zdieľať" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-sl.yml b/config/locales/crowdin/js-sl.yml index bcd2c62b88b8..a540f13a3838 100644 --- a/config/locales/crowdin/js-sl.yml +++ b/config/locales/crowdin/js-sl.yml @@ -150,6 +150,7 @@ sl: attribute_reference: macro_help_tooltip: "Ta del besedla je izveden dinamično s pomočjo makroprograma." not_found: "Zahtevanega vira ni mogoče najti" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "Izbrana lastnost '%{name}' ne obstaja." child_pages: button: "Povezave do podrejenih strani" @@ -905,6 +906,11 @@ sl: preformatted_text: "Predoblikovano besedilo" wiki_link: "Povezava do Wiki strani" image: "Slika" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Skupna sprememba projekta" @@ -1060,12 +1066,8 @@ sl: is_parent: "Datumi tega delovnega paketa so samodejno ugotovljeni od njegovih podrazredov. Za nastavitev datumov aktivirajte 'Ročno razvrščanje'." is_switched_from_manual_to_automatic: "Datumi delovnega paketa se bodo lahko, da morali preračunati po menjavi iz ročnega v samodejno razvrščanje zaradi razmerij z ostalimi delovnimi paketi." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-sr.yml b/config/locales/crowdin/js-sr.yml index 2130b8aa285d..d65f67c74dc3 100644 --- a/config/locales/crowdin/js-sr.yml +++ b/config/locales/crowdin/js-sr.yml @@ -150,6 +150,7 @@ sr: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -905,6 +906,11 @@ sr: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1060,12 +1066,8 @@ sr: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-sv.yml b/config/locales/crowdin/js-sv.yml index 70ed70fa4924..9a190baf580b 100644 --- a/config/locales/crowdin/js-sv.yml +++ b/config/locales/crowdin/js-sv.yml @@ -150,6 +150,7 @@ sv: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Länkar till underordnade sidor" @@ -903,6 +904,11 @@ sv: preformatted_text: "Förformaterad Text" wiki_link: "Länka till en Wiki-sida" image: "Bild" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk-ändra projekt" @@ -1058,12 +1064,8 @@ sv: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Dela" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-th.yml b/config/locales/crowdin/js-th.yml index 581a7fdeea4c..4e4841fd4084 100644 --- a/config/locales/crowdin/js-th.yml +++ b/config/locales/crowdin/js-th.yml @@ -150,6 +150,7 @@ th: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -903,6 +904,11 @@ th: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "รูปภาพ" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1058,12 +1064,8 @@ th: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "แชร์" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-tr.yml b/config/locales/crowdin/js-tr.yml index d619225ffc04..329558032eb7 100644 --- a/config/locales/crowdin/js-tr.yml +++ b/config/locales/crowdin/js-tr.yml @@ -150,6 +150,7 @@ tr: attribute_reference: macro_help_tooltip: "Bu metin parçası bir makro tarafından dinamik olarak işleniyor." not_found: "İstenen kaynak bulunamadı" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "Seçilen '%{name}' özniteliği mevcut değil." child_pages: button: "Alt sayfalara bağlantılar" @@ -903,6 +904,11 @@ tr: preformatted_text: "Önceden biçimlendirilmiş metin" wiki_link: "Bir Wiki sayfasına bağlantı" image: "Görsel" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Projenin toplu değişimi" @@ -1058,12 +1064,8 @@ tr: is_parent: "Bu çalışma paketinin tarihleri otomatik olarak çocuklarından çıkarılır. Tarihleri ayarlamak için 'Manuel zamanlama'yı etkinleştirin." is_switched_from_manual_to_automatic: "Bu çalışma paketinin tarihlerinin, diğer çalışma paketleriyle olan ilişkiler nedeniyle manuelden otomatik zamanlamaya geçtikten sonra yeniden hesaplanması gerekebilir." sharing: - share: "Paylaş" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-uk.yml b/config/locales/crowdin/js-uk.yml index a5b715fd3dc8..a2e637571def 100644 --- a/config/locales/crowdin/js-uk.yml +++ b/config/locales/crowdin/js-uk.yml @@ -150,6 +150,7 @@ uk: attribute_reference: macro_help_tooltip: "Цей текстовий фрагмент динамічно візуалізується макросом." not_found: "Не вдалося знайти потрібний ресурс" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "Вибраний атрибут «%{name}» не існує." child_pages: button: "Посилання на дочірні сторінки" @@ -906,6 +907,11 @@ uk: preformatted_text: "Попередньо відформатований текст" wiki_link: "Посилання на вікі-сторінку" image: "Зображення" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Групова зміна проєкту" @@ -1061,12 +1067,8 @@ uk: is_parent: "Дати цього пакета робіт автоматично виводяться з його дочірніх елементів. Активуйте «Ручне планування», щоб установити ці дати." is_switched_from_manual_to_automatic: "Можливо, дати цього пакета робіт знадобиться перерахувати після переходу з ручного на автоматичне планування через зв’язок з іншими пакетами робіт." sharing: - share: "Поширити" title: "Надати доступ до пакета робіт" show_all_users: "Показувати всіх користувачів, яким надано доступ до пакета робіт" - selected_count: "Вибрано: %{count}" - selection: - mixed: "Змішані" upsale: description: "Діліться пакетами робіт із користувачами, які не належать до учасників проєкту." table: diff --git a/config/locales/crowdin/js-uz.yml b/config/locales/crowdin/js-uz.yml index 1c8fa60ccaed..8b2d7caa73fe 100644 --- a/config/locales/crowdin/js-uz.yml +++ b/config/locales/crowdin/js-uz.yml @@ -150,6 +150,7 @@ uz: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -904,6 +905,11 @@ uz: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1059,12 +1065,8 @@ uz: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-vi.yml b/config/locales/crowdin/js-vi.yml index 3c20f1dbbb0a..e81ecf2f6cad 100644 --- a/config/locales/crowdin/js-vi.yml +++ b/config/locales/crowdin/js-vi.yml @@ -150,6 +150,7 @@ vi: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Liên kết đến trang con" @@ -902,6 +903,11 @@ vi: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1057,12 +1063,8 @@ vi: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Chia sẻ" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/locales/crowdin/js-zh-CN.yml b/config/locales/crowdin/js-zh-CN.yml index c9f8a5d3cd61..386edd142ffc 100644 --- a/config/locales/crowdin/js-zh-CN.yml +++ b/config/locales/crowdin/js-zh-CN.yml @@ -150,6 +150,7 @@ zh-CN: attribute_reference: macro_help_tooltip: "此文本段正由宏动态呈现。" not_found: "无法找到请求的资源" + nested_macro: "该宏递归引用 %{model} %{id}。" invalid_attribute: "所选属性 '%{name}' 不存在。" child_pages: button: "链接至子页面" @@ -902,6 +903,11 @@ zh-CN: preformatted_text: "预格式化文本" wiki_link: "链接到一个维基页面" image: "图片" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "批量更改项目" @@ -1057,12 +1063,8 @@ zh-CN: is_parent: "此工作包的日期会自动从其子项推导出。可激活“手动计划”来设置日期。" is_switched_from_manual_to_automatic: "由于与其他工作包的关系,在从手动计划切换为自动计划后,此工作包的日期可能需要重新计算。" sharing: - share: "共享" title: "共享工作包" show_all_users: "显示与之共享工作包的所有用户" - selected_count: "%{count} 已选择" - selection: - mixed: "混合" upsale: description: "与非此项目成员的用户共享工作包。" table: diff --git a/config/locales/crowdin/js-zh-TW.yml b/config/locales/crowdin/js-zh-TW.yml index 0f8313569a23..97419864f047 100644 --- a/config/locales/crowdin/js-zh-TW.yml +++ b/config/locales/crowdin/js-zh-TW.yml @@ -150,6 +150,7 @@ zh-TW: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "指向子頁面的連結" @@ -902,6 +903,11 @@ zh-TW: preformatted_text: "預先格式化文字" wiki_link: "連結到一個 Wiki 頁面" image: "圖片" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "專案整批變動" @@ -1057,12 +1063,8 @@ zh-TW: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "共享" title: "分享工作項目" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "已選取 %{count} 個" - selection: - mixed: "Mixed" upsale: description: "與不是專案成員的使用者共用工作項目。" table: diff --git a/config/locales/crowdin/ka.yml b/config/locales/crowdin/ka.yml index ba1b7761552f..dbe10d3b3de4 100644 --- a/config/locales/crowdin/ka.yml +++ b/config/locales/crowdin/ka.yml @@ -516,6 +516,10 @@ ka: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ ka: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ ka: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3359,54 +3368,56 @@ ka: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 მომხმარებელი" - one: "1 მომხმარებელი" - other: "%{count} მომხმარებელი" - filter: - project_member: "პროექტის წევრი" - not_project_member: "Not project member" - project_group: "პროექტის ჯგუფი" - not_project_group: "Not project group" - role: "როლი" - type: "ტიპი" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "კომენტარი" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "ჩასწორება" - edit_description: "Can view, comment and edit this work package." - view: "ხედი" - view_description: "Can view this work package." - remove: "წაშლა" - share: "გაზიარება" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "გაზიარებული არაა" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "დაბლოკილი მომხმარებელი" - invited: "მოსაწვევი გაიგზავნა. " - resend_invite: "თავიდან გაგზავნა." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/kk.yml b/config/locales/crowdin/kk.yml index d00f6d08bf0f..32bf4967bd75 100644 --- a/config/locales/crowdin/kk.yml +++ b/config/locales/crowdin/kk.yml @@ -516,6 +516,10 @@ kk: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ kk: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ kk: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3359,54 +3368,56 @@ kk: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Role" - type: "Type" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Comment" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Edit" - edit_description: "Can view, comment and edit this work package." - view: "View" - view_description: "Can view this work package." - remove: "Remove" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/ko.yml b/config/locales/crowdin/ko.yml index aba5b144bae2..80abb8b1d85d 100644 --- a/config/locales/crowdin/ko.yml +++ b/config/locales/crowdin/ko.yml @@ -508,6 +508,10 @@ ko: move: no_common_statuses_exists: "선택한 모든 작업 패키지에 사용 가능한 상태가 없습니다. 해당 상태를 변경할 수 없습니다." unsupported_for_multiple_projects: "여러 프로젝트에서 작업 패키지 일괄 이동/복사는 지원되지 않습니다." + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "작업 패키지 공유에 대한 워크플로 누락" @@ -907,6 +911,10 @@ ko: enabled_modules: dependency_missing: "'%{module}' 모듈이 '%{dependency}' 모듈에 의존하므로 해당 모듈도 활성화되어 있어야 합니다." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2562,6 +2570,7 @@ ko: notice_principals_found_multiple: "%{number} 개의 결과가 발견되었습니다.\n첫번째 결과를 보려면 탭 해주세요." notice_principals_found_single: "한 개의 결과가 있습니다.\n보려면 탭 해주세요." notice_project_not_deleted: "프로젝트가 삭제되지 않았습니다." + notice_project_not_found: "Project not found." notice_successful_connection: "연결에 성공했습니다." notice_successful_create: "생성에 성공했습니다." notice_successful_delete: "삭제에 성공했습니다." @@ -3319,54 +3328,56 @@ ko: work_based_help_text: "완료 %는 작업 및 남은 작업에서 자동으로 파생됩니다." status_based_help_text: "완료 %는 작업 패키지 상태에 따라 설정됩니다." migration_warning_text: "작업 기반 진행률 계산 모드에서 완료 %는 수동으로 설정할 수 없으며 작업에 연결됩니다. 기존 값은 유지되지만 편집할 수 없습니다. 먼저 작업을 입력하세요." - sharing: - count: - zero: "사용자 0명" - one: "사용자 1명" - other: "사용자 %{count}명" - filter: - project_member: "프로젝트 멤버" - not_project_member: "프로젝트 멤버 아님" - project_group: "프로젝트 그룹" - not_project_group: "프로젝트 그룹 없음" - role: "역할" - type: "타입" - label_search: "초대할 사용자 검색" - label_search_placeholder: "사용자 또는 이메일 주소로 검색" - label_toggle_all: "모든 공유 토글" - permissions: - comment: "코멘트" - comment_description: "이 작업 패키지를 보고 코멘트를 작성할 수 있습니다." - denied: "작업 패키지를 공유할 권한이 없습니다." - edit: "편집" - edit_description: "이 작업 패키지를 보고 코멘트를 작성하고 편집할 수 있습니다." - view: "보기" - view_description: "이 작업 패키지를 볼 수 있습니다." - remove: "제거" - share: "공유하기" - text_empty_search_description: "현재 필터 기준의 사용자가 없습니다." - text_empty_search_header: "일치하는 결과를 찾을 수 없습니다." - text_empty_state_description: "작업 패키지가 아직 누구와도 공유되지 않았습니다." - text_empty_state_header: "공유 안 됨" - text_user_limit_reached: "사용자를 더 추가하면 현재 한도가 초과됩니다. 외부 사용자가 이 작업 패키지에 액세스할 수 있도록 관리자에게 문의하여 사용자 한도를 늘리세요." - text_user_limit_reached_admins: '사용자를 더 추가하면 현재 한도가 초과됩니다. 사용자를 더 추가할 수 있도록 하려면 플랜을 업그레이드하세요.' - warning_user_limit_reached: > - 사용자를 더 추가하면 현재 한도가 초과됩니다. 외부 사용자가 이 작업 패키지에 액세스할 수 있도록 관리자에게 문의하여 사용자 한도를 늘리세요. - warning_user_limit_reached_admin: > - 사용자를 더 추가하면 현재 한도가 초과됩니다. 외부 사용자가 이 작업 패키지에 액세스할 수 있도록 하려면 플랜을 업그레이드하세요. - warning_no_selected_user: "이 작업 패키지를 공유할 사용자를 선택하세요" - warning_locked_user: "%{user} 사용자는 잠겨 있어 공유할 수 없습니다" - user_details: - locked: "잠긴 사용자" - invited: "초대장 전송됨. " - resend_invite: "다시 보내기." - invite_resent: "초대장을 다시 보냈습니다" - not_project_member: "프로젝트 멤버 아님" - project_group: "그룹 멤버는 (프로젝트 멤버로서) 추가적인 권한을 가질 수 있습니다" - not_project_group: "그룹(모든 멤버와 공유됨)" - additional_privileges_project: "(프로젝트 멤버로서) 추가적인 권한을 가질 수 있습니다" - additional_privileges_group: "(그룹 멤버로서) 추가적인 권한을 가질 수 있습니다" - additional_privileges_project_or_group: "(프로젝트 또는 그룹 멤버로서) 추가적인 권한을 가질 수 있습니다" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > 선택하지 않은 요일은 작업 패키지를 예약할 때 건너뜁니다(일 수에 포함되지 않음). 작업 패키지 수준에서 재정의할 수 있습니다. diff --git a/config/locales/crowdin/lt.yml b/config/locales/crowdin/lt.yml index f7ac4bcfa81c..5a29be0a5d60 100644 --- a/config/locales/crowdin/lt.yml +++ b/config/locales/crowdin/lt.yml @@ -527,6 +527,10 @@ lt: move: no_common_statuses_exists: "Parinkti darbo paketai neturi būsenos. Jų būsena negali pakeista." unsupported_for_multiple_projects: "Masinis darbų paketų perkėlimas/kopijavimas nėra palaikomas darbui iškart su keliais projektais" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Darbo paketo dalinimuisi trūksta darbo proceso" @@ -926,6 +930,10 @@ lt: enabled_modules: dependency_missing: "Reikia taip pat įjungti ir modulį '%{dependency}' nes modulis '%{module}' priklauso nuo jo." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2662,6 +2670,7 @@ lt: notice_principals_found_multiple: "Rasta %{number} rezultatai (-as, -ų).\nPaspauskite 'Tab', kad susikoncentruotumėte į pirmą rezultatą." notice_principals_found_single: "Yra vienas rezultatas.\nPaspauskite 'Tab', kad susikoncentruotumėte į jį." notice_project_not_deleted: "Projektas nebuvo panaikintas." + notice_project_not_found: "Project not found." notice_successful_connection: "Sėkmingas susijungimas." notice_successful_create: "Sėkmingas sukūrimas." notice_successful_delete: "Sėkmingas panaikinimas." @@ -3423,54 +3432,56 @@ lt: work_based_help_text: "% baigta automatiškai skaičiuojama pagal Darbą ir Likusį darbą." status_based_help_text: "% baigta nustatoma pagal paketo būseną." migration_warning_text: "Darbu paremtame eigos skaičiavimo režime % baigta negali būti nustatomas rankomis ir yra susietas su darbu. Esamos reikšmės buvo išlaikytos, bet negali būti keičiamos. Prašome iš pradžių įveskite darbą." - sharing: - count: - zero: "0 naudotojų" - one: "1 naudotojas" - other: "%{count} naudotojai" - filter: - project_member: "Projekto nariai" - not_project_member: "Ne projekto nariai" - project_group: "Projekto grupė" - not_project_group: "Ne projekto grupė" - role: "Vaidmuo" - type: "Tipas" - label_search: "Ieškoti naudotojų pakvietimui" - label_search_placeholder: "Ieškoti pagal naudotoją arba e-pašto adresą" - label_toggle_all: "Perjungti visus bendrinimus" - permissions: - comment: "Komentuoti" - comment_description: "Gali žiūrėti ir komentuoti šį darbo paketą." - denied: "Jūs neturite teisės benrinti darbo paketus." - edit: "Redaguoti" - edit_description: "Gali žiūrėti, komentuoti ir keisti šį darbo paketą." - view: "Žiūrėti" - view_description: "Gali žiūrėti šį darbo paketą." - remove: "Išimti" - share: "Dalintis" - text_empty_search_description: "Nėra naudotojų su dabartiniu filtro kriterijumi." - text_empty_search_header: "Mes neradome jokių atitinkančių rezultatų." - text_empty_state_description: "Darbo paketas dar nėra su niekuo bendrinamas." - text_empty_state_header: "Nesidalinama" - text_user_limit_reached: "Pridėjus papildomus naudotojus bus viršytas dabartinis apribojimas. Prašome susisiekti su administratoriumi ir padidinti naudotojų apribojimą, kad užtikrintumėte, jog išoriniai naudotojai gali prieiti prie šio darbo paketo." - text_user_limit_reached_admins: 'Pridėjus papildomus naudotojus bus viršytas dabartinis apribojimas. Prašome pagerinti jūsų planą, kad galėtumėte pridėti daugiau naudotojų.' - warning_user_limit_reached: > - Pridėjus papildomus naudotojus bus viršytas dabartinis apribojimas. Prašome susisiekti su administratoriumi ir padidinti naudotojų apribojimą, kad užtikrintumėte, jog išoriniai naudotojai gali prieiti prie šio darbo paketo. - warning_user_limit_reached_admin: > - Pridėjus papildomus naudotojus bus viršytas dabartinis apribojimas. Prašome pagerinti jūsų planą, kad užtikrintumėte, jog išoriniai naudotojai gali prieiti prie šio darbo paketo. - warning_no_selected_user: "Prašome parinkti naudotjus, su kuriais norite dalintis šiuo darbo paketu" - warning_locked_user: "Naudotojas %{user} yra užrakintas, todėl su juo negalima bendrinti" - user_details: - locked: "Užrakintas naudotojas" - invited: "Pakvietimas išsiųstas. " - resend_invite: "Siųsti iš naujo." - invite_resent: "Pakvietimas išsiųstas iš naujo" - not_project_member: "Ne projekto narys" - project_group: "Grupės nariai gali turėti papildomų privilegijų (kaip projekto nariai)" - not_project_group: "Grupė (bendra visiems nariams)" - additional_privileges_project: "Gali turėti papildomų privilegijų (kaip projekto narys)" - additional_privileges_group: "Gali turėti papildomų privilegijų (kaip grupės narys)" - additional_privileges_project_or_group: "Gali turėti papildomų privilegijų (kaip projekto ar grupės narys)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/lv.yml b/config/locales/crowdin/lv.yml index 55844ed97c82..649a516764b3 100644 --- a/config/locales/crowdin/lv.yml +++ b/config/locales/crowdin/lv.yml @@ -523,6 +523,10 @@ lv: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Vairāku darba paku pārvietošana/kopēšana netiek atbalstīta no vairākiem projektiem" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -922,6 +926,10 @@ lv: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2631,6 +2639,7 @@ lv: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3395,54 +3404,56 @@ lv: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Loma" - type: "Veids" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Komentârs" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Labot" - edit_description: "Can view, comment and edit this work package." - view: "View" - view_description: "Can view this work package." - remove: "Noņemt" - share: "Koplietot" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/mn.yml b/config/locales/crowdin/mn.yml index 94a8aee108c7..5aca96412445 100644 --- a/config/locales/crowdin/mn.yml +++ b/config/locales/crowdin/mn.yml @@ -516,6 +516,10 @@ mn: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ mn: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ mn: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3359,54 +3368,56 @@ mn: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Role" - type: "Type" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Comment" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Edit" - edit_description: "Can view, comment and edit this work package." - view: "View" - view_description: "Can view this work package." - remove: "Remove" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/ms.seeders.yml b/config/locales/crowdin/ms.seeders.yml index 9391625e2fb1..413282b21c07 100644 --- a/config/locales/crowdin/ms.seeders.yml +++ b/config/locales/crowdin/ms.seeders.yml @@ -43,9 +43,9 @@ ms: name: Lain-lain project_query_roles: item_0: - name: Project query viewer + name: Pemerhati pertanyaan projek item_1: - name: Project query editor + name: Pengedit pertanyaan projek work_package_roles: item_0: name: Pengedit pakej kerja diff --git a/config/locales/crowdin/ms.yml b/config/locales/crowdin/ms.yml index f5f65b4b2a53..4f9680326a9d 100644 --- a/config/locales/crowdin/ms.yml +++ b/config/locales/crowdin/ms.yml @@ -303,7 +303,7 @@ ms: add_projects: Tambah projek include_sub_projects: Sertakan sub-projek project_mappings: - header: Enabled in projects + header: Didayakan dalam projek types: no_results_title_text: Tiada jenis yang tersedia buat masa ini. form: @@ -460,7 +460,7 @@ ms: is_readonly: "Baca-sahaja" excluded_from_totals: "Dikecualikan daripada jumlah" themes: - dark: "Dark" + dark: "Gelap" light: "Terang" light_high_contrast: "Kontras tinggi yang terang" types: @@ -507,6 +507,10 @@ ms: move: no_common_statuses_exists: "Tiada status tersedia untuk semua pakej kerja yang terpilih. Status mereka tidak dapat diubah." unsupported_for_multiple_projects: "Pindahan/salinan secara pukal tidak disokong untuk pakej kerja dari projek yang berbeza" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Aliran kerja hilang untuk perkongsian pakej kerja" @@ -906,6 +910,10 @@ ms: enabled_modules: dependency_missing: "Modul '%{dependency}' perlu dibenarkan juga kerana modul '%{module}' bergantung padanya." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2561,6 +2569,7 @@ ms: notice_principals_found_multiple: "Terdapat %{number} hasil ditemui. \n Tab untuk memfokuskan hasil pertama." notice_principals_found_single: "Terdapat satu hasil. \n Tab untuk fokuskan." notice_project_not_deleted: "Projek tersebut tidak dipadam." + notice_project_not_found: "Project not found." notice_successful_connection: "Sambungan berjaya." notice_successful_create: "Penciptaan berjaya." notice_successful_delete: "Pemadaman berjaya." @@ -2694,8 +2703,8 @@ ms: permission_manage_public_bcf_queries: "Urus pertanyaan BCF awam" permission_edit_attribute_help_texts: "Edit atribut teks bantuan" permission_manage_public_project_queries: "Uruskan senarai projek awam" - permission_view_project_query: "View project query" - permission_edit_project_query: "Edit project query" + permission_view_project_query: "Lihat pertanyaan projek" + permission_edit_project_query: "Edit pertanyaan projek" placeholders: default: "-" project: @@ -3319,54 +3328,56 @@ ms: work_based_help_text: "% Selesai secara automatik diperolehi daripada Kerja dan Kerja yang berbaki." status_based_help_text: "% Selesai ditetapkan oleh status pakej kerja." migration_warning_text: "Dalam mod pengiraan perkembangan berdasarkan kerja, % Selesai tidak boleh ditetapkan secara manual dan ianya terikat kepada Kerja. Nilai sedia ada tersebut telah disimpan tetapi tidak boleh diedit. Sila input Kerja dahulu." - sharing: - count: - zero: "0 pengguna" - one: "1 pengguna" - other: "%{count} pengguna" - filter: - project_member: "Ahli projek" - not_project_member: "Bukan ahli projek" - project_group: "Kumpulan projek" - not_project_group: "Bukan kumpulan projek" - role: "Peranan" - type: "Jenis" - label_search: "Cari pengguna untuk jemput" - label_search_placeholder: "Cari mengikut pengguna atau alamat e-mel" - label_toggle_all: "Tukar semua perkongsian" - permissions: - comment: "Komen" - comment_description: "Boleh lihat dan komen berkenaan pakej kerja ini." - denied: "Anda tidak mempunyai kebenaran untuk berkongsi pakej kerja." - edit: "Edit" - edit_description: "Boleh lihat, komen dan edit pakej kerja ini." - view: "Lihat" - view_description: "Boleh papar pakej kerja ini." - remove: "Keluarkan" - share: "Kongsi" - text_empty_search_description: "Tiada pengguna dengan kriteria penyaring semasa." - text_empty_search_header: "Kami tidak menemui keputusan yang sepadan." - text_empty_state_description: "Pakej kerja masih belum dikongsikan dengan sesiapa lagi." - text_empty_state_header: "Tidak dikongsikan" - text_user_limit_reached: "Menambah pengguna tambahan akan melebihi had semasa. Sila hubungi pentadbir untuk meningkatkan had pengguna bagi memastikan pengguna luaran dapat mengakses pakej kerja ini." - text_user_limit_reached_admins: 'Menambah pengguna tambahan akan melebihi had semasa. Sila naik taraf pelan anda untuk membolehkan untuk menambah lebih banyak pengguna.' - warning_user_limit_reached: > - Menambah pengguna tambahan akan melebihi had semasa. Sila hubungi pentadbir untuk meningkatkan had pengguna bagi memastikan pengguna luaran dapat mengakses pakej kerja ini. - warning_user_limit_reached_admin: > - Menambah pengguna tambahan akan melebihi had semasa. Sila naik taraf pelan anda bagi memastikan pengguna luaran dapat mengakses pakej kerja ini. - warning_no_selected_user: "Sila pilih pengguna untuk berkongsi pakej kerja ini dengan mereka" - warning_locked_user: "Pengguna %{user} telah dikunci dan tidak boleh dikongsi" - user_details: - locked: "Pengguna yang dikunci" - invited: "Jemputan dihantar. " - resend_invite: "Hantar semula." - invite_resent: "Jemputan telah dihantar semula" - not_project_member: "Bukan ahli projek" - project_group: "Ahli kumpulan mungkin mempunyai keistimewaan tambahan (sebagai ahli projek)" - not_project_group: "Kumpulan (dikongsi dengan semua ahli)" - additional_privileges_project: "Mungkin mempunyai keistimewaan tambahan (sebagai ahli projek)" - additional_privileges_group: "Mungkin mempunyai keistimewaan tambahan (sebagai ahli kumpulan)" - additional_privileges_project_or_group: "Mungkin mempunyai keistimewaan tambahan (sebagai ahli projek atau kumpulan)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Hari yang tidak dipilih akan dilangkau semasa penjadualan pakej kerja (dan tidak termasuk dalam kiraan hari). Ini boleh digantikan di peringkat pakej-kerja. diff --git a/config/locales/crowdin/ne.yml b/config/locales/crowdin/ne.yml index c5ccaa409646..d2228f7b7d75 100644 --- a/config/locales/crowdin/ne.yml +++ b/config/locales/crowdin/ne.yml @@ -516,6 +516,10 @@ ne: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ ne: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ ne: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3359,54 +3368,56 @@ ne: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Role" - type: "Type" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Comment" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Edit" - edit_description: "Can view, comment and edit this work package." - view: "View" - view_description: "Can view this work package." - remove: "Remove" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/nl.yml b/config/locales/crowdin/nl.yml index f98b76490664..f8ae2748611b 100644 --- a/config/locales/crowdin/nl.yml +++ b/config/locales/crowdin/nl.yml @@ -513,6 +513,10 @@ nl: move: no_common_statuses_exists: "Er is geen status beschikbaar voor alle geselecteerde werkpakketten. Hun status kan niet worden gewijzigd." unsupported_for_multiple_projects: "Bulk verplaatsen/kopiëren wordt niet ondersteund voor werkpakketten uit meerdere projecten" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -912,6 +916,10 @@ nl: enabled_modules: dependency_missing: "De module '%{dependency}' moet ook ingeschakeld zijn, omdat de module '%{module}' ervan afhankelijk is." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2593,6 +2601,7 @@ nl: notice_principals_found_multiple: "Er zijn %{number} resultaten gevonden. Tab om het eerste resultaat te tonen." notice_principals_found_single: "Er is één resultaat. Tab om te tonen." notice_project_not_deleted: "Het project is niet verwijderd." + notice_project_not_found: "Project not found." notice_successful_connection: "Geslaagde verbinding." notice_successful_create: "Aanmaak geslaagd." notice_successful_delete: "Verwijdering geslaagd." @@ -3354,54 +3363,56 @@ nl: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Rol" - type: "Type" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Commentaar" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Wijzig" - edit_description: "Can view, comment and edit this work package." - view: "Toon" - view_description: "Can view this work package." - remove: "Verwijder" - share: "Deel" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Niet gedeeld" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/no.yml b/config/locales/crowdin/no.yml index 5a470ee27a6d..e5c948e81c1b 100644 --- a/config/locales/crowdin/no.yml +++ b/config/locales/crowdin/no.yml @@ -515,6 +515,10 @@ move: no_common_statuses_exists: "Det er ingen status tilgjengelig for alle valgte arbeidspakker. Status kan ikke endres." unsupported_for_multiple_projects: "Bulk flytt/kopi er ikke støttet for arbeidspakker fra flere prosjekter" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Arbeidsflyt mangler for deling av arbeidspakker" @@ -914,6 +918,10 @@ enabled_modules: dependency_missing: "Modulen '%{dependency}' må være aktivert i tillegg siden modulen '%{module}' avhenger av den." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2596,6 +2604,7 @@ notice_principals_found_multiple: "Det finnes %{number} resultater. \n bruk tab for å fokusere det første resultatet." notice_principals_found_single: "Det er ett resultat. \n Bruk tab for å fokusere det." notice_project_not_deleted: "Prosjektet ble ikke slettet." + notice_project_not_found: "Project not found." notice_successful_connection: "Vellykket tilkobling." notice_successful_create: "Opprettelsen var vellykket." notice_successful_delete: "Slettingen var vellykket." @@ -3358,54 +3367,56 @@ work_based_help_text: "% Ferdig utledes automatisk fra arbeid og gjenstående arbeid." status_based_help_text: "% Ferdig er angitt etter status på arbeidspakken." migration_warning_text: "I arbeidsbasert fremdriftsberegningsmodus kan % Ferdig ferdigstilt ikke settes manuelt og er knyttet til jobber. Den eksisterende verdien er lagret, men kan ikke endres. Skriv inn arbeidet først." - sharing: - count: - zero: "0 brukere" - one: "1 bruker" - other: "%{count} brukere" - filter: - project_member: "Prosjektmedlem" - not_project_member: "Ikke prosjektmedlem" - project_group: "Prosjektgruppe" - not_project_group: "Ikke prosjektgruppe" - role: "Rolle" - type: "Type" - label_search: "Søk etter brukere for å invitere" - label_search_placeholder: "Søk med bruker eller e-postadresse" - label_toggle_all: "Veksle melom delinger" - permissions: - comment: "Kommentar" - comment_description: "Kan se og kommentere denne arbeidspakken." - denied: "Du har ikke tillatelse til å dele arbeidspakker." - edit: "Rediger" - edit_description: "Kan vise, kommentere og redigere denne arbeidspakken." - view: "Vis" - view_description: "Kan se denne arbeidspakken." - remove: "Fjern" - share: "Del" - text_empty_search_description: "Det er ingen brukere med gjeldende filterkriterier." - text_empty_search_header: "Vi kunne ikke finne noen matchende resultater." - text_empty_state_description: "Arbeidspakken har ikke blitt delt med noen enda." - text_empty_state_header: "Ikke delt" - text_user_limit_reached: "Å legg til ekstra brukere vil overskride gjeldende grense. Vennligst kontakt en administrator for å øke brukergrensen for å sikre at eksterne brukere har tilgang til denne arbeidspakken." - text_user_limit_reached_admins: 'Å legge til ekstra brukere overskrider gjeldende grense. oppgradere din plan for å kunne legge til flere brukere.' - warning_user_limit_reached: > - Å legg til ekstra brukere vil overskride gjeldende grense. Vennligst kontakt en administrator for å øke brukergrensen for å sikre at eksterne brukere har tilgang til denne arbeidspakken. - warning_user_limit_reached_admin: > - Å legg til ekstra brukere vil overskride gjeldende grense. Vennligst oppgrader planen din for å sikre at eksterne brukere har tilgang til denne arbeidspakken. - warning_no_selected_user: "Velg brukere du skal dele denne arbeidspakken med" - warning_locked_user: "Brukeren %{user} er låst og kan ikke bli delt med" - user_details: - locked: "Låst bruker" - invited: "Invitasjon sendt. " - resend_invite: "Send på nytt" - invite_resent: "Invitasjonen er sendt på nytt" - not_project_member: "Ikke et prosjektmedlem" - project_group: "Gruppemedlemmer kan ha flere privilegier (som prosjektmedlemmer)" - not_project_group: "Gruppe (delt med alle medlemmer)" - additional_privileges_project: "Kan ha flere privilegier (som prosjektmedlem)" - additional_privileges_group: "Kan ha flere privilegier (som gruppemedlem)" - additional_privileges_project_or_group: "Kan ha flere privilegier (som prosjekt- eller gruppemedlem)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Hoppet over dager som ikke er valgt ved planlegging av arbeidspakker (og ikke inkludert i antall dager). Disse kan overstyres på arbeidspakkenivå. diff --git a/config/locales/crowdin/pl.yml b/config/locales/crowdin/pl.yml index 1f805f38d647..6f55c689af25 100644 --- a/config/locales/crowdin/pl.yml +++ b/config/locales/crowdin/pl.yml @@ -527,6 +527,10 @@ pl: move: no_common_statuses_exists: "Brak stanu dostępnego dla wszystkich wybranych pakietów roboczych. Ich stanu nie można zmienić." unsupported_for_multiple_projects: "Hurtowe przenoszenie / kopiowanie nie jest obsługiwane dla Zestawu zadań z różnych projektów" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Brak przepływu pracy udostępniania pakietu roboczego" @@ -926,6 +930,10 @@ pl: enabled_modules: dependency_missing: "Moduł „%{dependency}” musi również zostać włączony, ponieważ zależny jest od niego moduł „%{module}”." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2661,6 +2669,7 @@ pl: notice_principals_found_multiple: "Znaleziono %{number} wyników.\nNaciśnij Tab, aby wybrać pierwszy rezultat." notice_principals_found_single: "Znaleziono 1 wynik.\nNaciśnij Tab, aby go wybrać." notice_project_not_deleted: "Projekt nie został usunięty." + notice_project_not_found: "Project not found." notice_successful_connection: "Połączenie zakończone sukcesem." notice_successful_create: "Tworzenie zakończone sukcesem." notice_successful_delete: "Usuwanie zakończone sukcesem." @@ -3425,54 +3434,56 @@ pl: work_based_help_text: "% ukończenia jest automatycznie wyprowadzany z wartości Praca i Pozostała praca." status_based_help_text: "% ukończenia jest ustawiany na podstawie statusu pakietu roboczego." migration_warning_text: "W trybie obliczania postępu na podstawie pracy wartości % ukończenia nie można ustawić ręcznie i jest ona powiązana z wartością Praca. Istniejąca wartość została zachowana, ale nie można jej edytować. Najpierw wprowadź wartość Praca." - sharing: - count: - zero: "0 użytkowników" - one: "1 użytkownik" - other: "%{count} użytkowników" - filter: - project_member: "Członek projektu" - not_project_member: "Nie jest członkiem projektu" - project_group: "Grupa projektu" - not_project_group: "Nie jest grupą projektu" - role: "Rola" - type: "Typ" - label_search: "Wyszukaj użytkowników do zaproszenia" - label_search_placeholder: "Wyszukaj użytkownika lub adres e-mail" - label_toggle_all: "Przełącz wszystkie udostępnienia" - permissions: - comment: "Komentarz" - comment_description: "Może wyświetlać i komentować ten pakiet roboczy." - denied: "Nie masz uprawnień do udostępniania pakietów roboczych." - edit: "Edycja" - edit_description: "Może wyświetlać, komentować i edytować ten pakiet roboczy." - view: "Widok" - view_description: "Może wyświetlić ten pakiet roboczy." - remove: "Usuń" - share: "Udostępnij" - text_empty_search_description: "Brak użytkowników spełniających bieżące kryteria filtrowania." - text_empty_search_header: "Nie mogliśmy znaleźć żadnych pasujących wyników." - text_empty_state_description: "Pakiet roboczy nie został jeszcze nikomu udostępniony." - text_empty_state_header: "Nie udostępniono" - text_user_limit_reached: "Dodanie dodatkowych użytkowników spowoduje przekroczenie bieżącego limitu. Aby zapewnić użytkownikom zewnętrznym dostęp do tego pakietu roboczego, skontaktuj się z administratorem w celu zwiększenia limitu liczby użytkowników." - text_user_limit_reached_admins: 'Dodanie dodatkowych użytkowników spowoduje przekroczenie bieżącego limitu. Aby móc dodać więcej użytkowników, przejdź na wyższy plan.' - warning_user_limit_reached: > - Dodanie dodatkowych użytkowników spowoduje przekroczenie bieżącego limitu. Aby zapewnić użytkownikom zewnętrznym dostęp do tego pakietu roboczego, skontaktuj się z administratorem w celu zwiększenia limitu liczby użytkowników. - warning_user_limit_reached_admin: > - Dodanie dodatkowych użytkowników spowoduje przekroczenie bieżącego limitu. Aby zapewnić użytkownikom zewnętrznym dostęp do tego pakietu roboczego, przejdź na wyższy plan. - warning_no_selected_user: "Wybierz użytkowników, którym chcesz udostępnić ten pakiet roboczy" - warning_locked_user: "Użytkownik %{user} jest zablokowany i nie może być udostępniony" - user_details: - locked: "Zablokowany użytkownik" - invited: "Wysłano zaproszenie. " - resend_invite: "Wyślij ponownie. " - invite_resent: "Zaproszenie zostało ponownie wysłane" - not_project_member: "Nie jest członkiem projektu" - project_group: "Członkowie grupy mogą mieć dodatkowe uprawnienia (jako członkowie projektu)" - not_project_group: "Grupa (udostępniona wszystkim członkom)" - additional_privileges_project: "Może mieć dodatkowe uprawnienia (jako członek projektu)" - additional_privileges_group: "Może mieć dodatkowe uprawnienia (jako członek grupy)" - additional_privileges_project_or_group: "Może mieć dodatkowe uprawnienia (jako członek projektu lub grupy)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Dni, które nie zostały wybrane, są pomijane podczas planowania pakietów roboczych (i nie są uwzględnione w liczbie dni). Mogą one zostać zastąpione na poziomie pakietu roboczego. diff --git a/config/locales/crowdin/pt-BR.yml b/config/locales/crowdin/pt-BR.yml index 23c96acb3fea..4c6e6a0c5a09 100644 --- a/config/locales/crowdin/pt-BR.yml +++ b/config/locales/crowdin/pt-BR.yml @@ -514,6 +514,10 @@ pt-BR: move: no_common_statuses_exists: "Não há situação disponível para todos os pacotes de trabalho selecionados. Sua situação não pode ser alterada." unsupported_for_multiple_projects: "Movimentação/cópia em massa não é suportada para pacotes de trabalho de vários projetos" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Fluxo de trabalho ausente para compartilhamento de pacotes de trabalho" @@ -913,6 +917,10 @@ pt-BR: enabled_modules: dependency_missing: "O módulo '%{dependency}' precisa ser habilitado, pois o módulo '%{module}' depende dele." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2594,6 +2602,7 @@ pt-BR: notice_principals_found_multiple: "Existe(m) %{number} resultado(s) encontrado(s).\nTecle tab para ir ao primeiro resultado." notice_principals_found_single: "Existe um resultado. Tecle tab para ir para ele." notice_project_not_deleted: "O projeto não foi excluído." + notice_project_not_found: "Project not found." notice_successful_connection: "Conectado com sucesso." notice_successful_create: "Criado com sucesso." notice_successful_delete: "Exclusão bem sucedida." @@ -3354,54 +3363,56 @@ pt-BR: work_based_help_text: "% de conclusão é automaticamente calculada com base no trabalho total e no trabalho restante." status_based_help_text: "A % de conclusão é definida pelo estado do pacote de trabalho." migration_warning_text: "No modo de cálculo de progresso com base no trabalho, a % conclusão não pode ser definida manualmente e está vinculada ao Trabalho. O valor existente foi mantido, mas não pode ser editado. Favor inserir o Trabalho primeiro." - sharing: - count: - zero: "0 usuários" - one: "1 usuário" - other: "%{count} usuários" - filter: - project_member: "Membro do projeto" - not_project_member: "Nenhum membro do projeto" - project_group: "Grupo do projeto" - not_project_group: "Não é um grupo do projeto" - role: "Papel" - type: "Tipo" - label_search: "Buscar usuários para convidar" - label_search_placeholder: "Buscar por usuário ou endereço de e-mail" - label_toggle_all: "Alternar todos os compartilhamentos" - permissions: - comment: "Comentário" - comment_description: "Pode visualizar e comentar neste pacote de trabalho." - denied: "Você não possui permissões para compartilhar pacotes de trabalho." - edit: "Editar" - edit_description: "Pode visualizar, comentar e editar este pacote de trabalho." - view: "Ver" - view_description: "Pode visualizar este pacote de trabalho." - remove: "Remover" - share: "Compartilhar" - text_empty_search_description: "Não há usuários com o critério de filtro atual." - text_empty_search_header: "Não encontramos nenhum resultado correspondente." - text_empty_state_description: "O pacote de trabalho ainda não foi compartilhado com ninguém." - text_empty_state_header: "Não compartilhado" - text_user_limit_reached: "A adição de usuários adicionais fará com que o limite atual seja excedido. Entre em contato com um administrador para aumentar o limite de usuários e garantir que usuários externos possam acessar este pacote de trabalho." - text_user_limit_reached_admins: 'A adição de usuários adicionais excederá o limite atual. Atualize o seu plano para poder adicionar mais usuários.' - warning_user_limit_reached: > - A adição de usuários adicionais fará com que o limite atual seja excedido. Entre em contato com um administrador para aumentar o limite de usuários e garantir que usuários externos possam acessar este pacote de trabalho. - warning_user_limit_reached_admin: > - A adição de usuários adicionais excederá o limite atual. Atualize o seu plano para poder garantir que os usuários externos possam acessar a este pacote de trabalho. - warning_no_selected_user: "Selecione os usuários com quem compartilhar este pacote de trabalho" - warning_locked_user: "O usuário %{user} está bloqueado e não pode ser compartilhado com" - user_details: - locked: "Usuário bloqueado" - invited: "Convite enviado. " - resend_invite: "Reenviar." - invite_resent: "O convite foi reenviado" - not_project_member: "Nenhum membro do projeto" - project_group: "Os membros do grupo podem ter privilégios adicionais (como membros do projeto)" - not_project_group: "Grupo (compartilhado com todos os membros)" - additional_privileges_project: "Poderá ter privilégios adicionais (como membro do projeto)" - additional_privileges_group: "Poderá ter privilégios adicionais (como membro do grupo)" - additional_privileges_project_or_group: "Poderá ter privilégios adicionais (como membro do projeto ou grupo)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Dias não selecionados são ignorados ao agendar pacotes de trabalho (e não são incluídos na contagem de dias). Isso pode ser alterado individualmente em cada pacote de trabalho. diff --git a/config/locales/crowdin/pt-PT.yml b/config/locales/crowdin/pt-PT.yml index 8ecc028f0b51..344e21b780f2 100644 --- a/config/locales/crowdin/pt-PT.yml +++ b/config/locales/crowdin/pt-PT.yml @@ -514,6 +514,10 @@ pt-PT: move: no_common_statuses_exists: "Não há nenhum status disponível para todos os pacotes de trabalho selecionados. O seu status não pode ser alterado." unsupported_for_multiple_projects: "Mover/copiar em massa não é suportado por pacotes de trabalho de vários projetos" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Falta um fluxo de trabalho para a partilha de pacotes de trabalho" @@ -913,6 +917,10 @@ pt-PT: enabled_modules: dependency_missing: "O módulo '%{dependency}' tem de ser ativado também, pois o módulo '%{module}' depende dele." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2594,6 +2602,7 @@ pt-PT: notice_principals_found_multiple: "Foram encontrados %{number} resultados. Carregue em TAB para ver o primeiro resultado." notice_principals_found_single: "Foi encontrado um resultado. Carregue em TAB para vê-lo." notice_project_not_deleted: "O projeto não foi eliminado." + notice_project_not_found: "Project not found." notice_successful_connection: "Ligação bem sucedida." notice_successful_create: "Criado com sucesso." notice_successful_delete: "Eliminado com sucesso." @@ -3354,54 +3363,56 @@ pt-PT: work_based_help_text: "A % de conclusão é derivada automaticamente do Trabalho e do Trabalho restante." status_based_help_text: "A % de conclusão é definida pelo estado do pacote de trabalho." migration_warning_text: "No modo de cálculo do progresso com base no trabalho, a % de conclusão não pode ser definida manualmente e está ligada ao Trabalho. O valor existente foi mantido, mas não pode ser editado. Introduza primeiro o Trabalho." - sharing: - count: - zero: "0 usuários" - one: "1 usuário" - other: "%{count} usuários" - filter: - project_member: "Membro do projeto" - not_project_member: "Não é um membro do projeto" - project_group: "Grupo do projeto" - not_project_group: "Não é um grupo do projeto" - role: "Função" - type: "Tipo" - label_search: "Pesquisar utilizadores para convidar" - label_search_placeholder: "Pesquisar por utilizador ou endereço de e-mail" - label_toggle_all: "Alternar todas as partilhas" - permissions: - comment: "Comentario" - comment_description: "Pode ver e comentar este pacote de trabalho." - denied: "Não tem permissões para partilhar pacotes de trabalho." - edit: "Editar" - edit_description: "Pode ver, comentar e editar este pacote de trabalho." - view: "Ver" - view_description: "Pode ver este pacote de trabalho." - remove: "Remover" - share: "Partilhe" - text_empty_search_description: "Não há utilizadores com o critério de filtro atual." - text_empty_search_header: "Não conseguimos encontrar nenhum resultado correspondente." - text_empty_state_description: "O pacote de trabalho ainda não foi partilhado com ninguém." - text_empty_state_header: "Não partilhado" - text_user_limit_reached: "A adição de utilizadores adicionais excederá o limite atual. Contacte um administrador para aumentar o limite de utilizadores, de modo a garantir que os utilizadores externos possam aceder a este pacote de trabalho." - text_user_limit_reached_admins: 'A adição de utilizadores adicionais excederá o limite atual. Atualize o seu plano para poder adicionar mais utilizadores.' - warning_user_limit_reached: > - A adição de utilizadores adicionais excederá o limite atual. Contacte um administrador para aumentar o limite de utilizadores, de modo a garantir que os utilizadores externos possam aceder a este pacote de trabalho. - warning_user_limit_reached_admin: > - A adição de utilizadores adicionais excederá o limite atual. Atualize o seu plano para poder garantir que os utilizadores externos possam aceder a este pacote de trabalho. - warning_no_selected_user: "Selecione os utilizadores com quem partilhar este pacote de trabalho" - warning_locked_user: "O utilizador %{user} está bloqueado e não pode ser partilhado com" - user_details: - locked: "Utilizador bloqueado" - invited: "Convite enviado." - resend_invite: "Reenviar." - invite_resent: "O convite foi reenviado" - not_project_member: "Não é um membro do projeto" - project_group: "Os membros do grupo podem ter privilégios adicionais (como membros do projeto)" - not_project_group: "Grupo (partilhado com todos os membros)" - additional_privileges_project: "Poderá ter privilégios adicionais (como membro do projeto)" - additional_privileges_group: "Poderá ter privilégios adicionais (como membro do grupo)" - additional_privileges_project_or_group: "Poderá ter privilégios adicionais (como membro do projeto ou do grupo)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Os dias não selecionados são ignorados ao agendar pacotes de trabalho (e não são incluídos na contagem de dias). Podem ser substituídos ao nível dos pacotes de trabalho. diff --git a/config/locales/crowdin/ro.yml b/config/locales/crowdin/ro.yml index f9e04c7160a4..891f2102e64c 100644 --- a/config/locales/crowdin/ro.yml +++ b/config/locales/crowdin/ro.yml @@ -523,6 +523,10 @@ ro: move: no_common_statuses_exists: "Nu există o stare disponibilă pentru toate pachetele de lucru selectate. Starea acestora nu poate fi modificată." unsupported_for_multiple_projects: "Mutarea/copierea în masă nu este suportată pentru pachete de lucru din proiecte multiple" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -922,6 +926,10 @@ ro: enabled_modules: dependency_missing: "Modulul \"%{dependency}\" trebuie, de asemenea, să fie activat, deoarece modulul \"%{module}\" depinde de el." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2630,6 +2638,7 @@ ro: notice_principals_found_multiple: "S-au găsit %{number} rezultate\n Tab pentru a se concentra pe primul rezultat." notice_principals_found_single: "Există un singur rezultat\n Tab pentru a-l concentra." notice_project_not_deleted: "Proiectul nu a fost şters." + notice_project_not_found: "Project not found." notice_successful_connection: "Conectare reușită." notice_successful_create: "Creare reuşită." notice_successful_delete: "Ştergere reuşită." @@ -3394,54 +3403,56 @@ ro: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Rol" - type: "Tip" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Comentariu" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Editare" - edit_description: "Can view, comment and edit this work package." - view: "Vizualizare" - view_description: "Can view this work package." - remove: "Eliminare" - share: "Distribuiți" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Nu este publică" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/ru.yml b/config/locales/crowdin/ru.yml index c27d882453ff..16818f1531af 100644 --- a/config/locales/crowdin/ru.yml +++ b/config/locales/crowdin/ru.yml @@ -529,6 +529,10 @@ ru: move: no_common_statuses_exists: "Статус не доступен для всех выбранных пакетов работ. Их статус изменен быть не может." unsupported_for_multiple_projects: "Массовое перемещение/копирование не поддерживается при работе с пакетами из нескольких проектов" + current_type_not_available_in_target_project: > + Текущий тип пакета работ не включен в целевой проект. Пожалуйста, включите этот тип в целевом проекте, если хотите, чтобы он остался неизменным. В противном случае тип пакета работ будет автоматически переназначен, что может привести к потере данных. + bulk_current_type_not_available_in_target_project: > + Текущие типы пакетов работ не включены в целевой проект. Включите типы в целевом проекте, если хотите, чтобы они остались неизменными. В противном случае типы пакетов работ будут автоматически переназначены, что может привести к потере данных. sharing: missing_workflow_warning: title: "Отсутствует рабочий процесс для совместного использования пакета работ" @@ -928,6 +932,10 @@ ru: enabled_modules: dependency_missing: "Модуль '%{dependency}' ' должен быть включен, так как модуль '%{module}' зависит от него." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Пожалуйста, выберите проект." query: attributes: project: @@ -2663,6 +2671,7 @@ ru: notice_principals_found_multiple: "Найдено %{number} результатов. Нажмите для перехода к первому." notice_principals_found_single: "Найден один результат. Нажмите для перехода к нему." notice_project_not_deleted: "Проект удалён не был." + notice_project_not_found: "Проект не найден." notice_successful_connection: "Подключение выполнено." notice_successful_create: "Создание выполнено." notice_successful_delete: "Удаление выполнено." @@ -3426,54 +3435,56 @@ ru: work_based_help_text: "% Выполнения автоматически выводится из Работ и Оставшихся работ." status_based_help_text: "% Выполнения определяется статусом пакета работ." migration_warning_text: "В режиме расчета прогресса \"На основе трудозатрат\" процент завершения невозможно установить вручную, он привязан к трудозатратам. Существующее значение сохранено, но его нельзя изменить. Пожалуйста, сначала введите трудозатраты." - sharing: - count: - zero: "0 пользователей" - one: "1 пользователь" - other: "%{count} пользователей" - filter: - project_member: "Участник проекта" - not_project_member: "Не участник проекта" - project_group: "Группа проекта" - not_project_group: "Не группа проекта" - role: "Роль" - type: "Тип" - label_search: "Искать пользователей для приглашения" - label_search_placeholder: "Поиск по пользователю или адресу электронной почты" - label_toggle_all: "Переключить все общие ресурсы" - permissions: - comment: "Комментарий" - comment_description: "Может просматривать и комментировать этот пакет работ." - denied: "У вас нет прав на предоставление общего доступа к пакетам работ." - edit: "Правка" - edit_description: "Может просматривать, комментировать и редактировать этот пакет работ." - view: "Просмотр" - view_description: "Может просматривать этот пакет работ." - remove: "Удалить" - share: "Поделиться" - text_empty_search_description: "Пользователей с текущими критериями фильтрации нет." - text_empty_search_header: "Не удалось найти подходящие результаты." - text_empty_state_description: "Пакет работ еще не передан никому." - text_empty_state_header: "Без общего доступа" - text_user_limit_reached: "Добавление дополнительных пользователей превысит текущий лимит. Пожалуйста, свяжитесь с администратором, чтобы увеличить лимит пользователей, чтобы убедиться, что внешние пользователи имеют доступ к этому пакету работ." - text_user_limit_reached_admins: 'Добавление дополнительных пользователей превысит текущий лимит. Пожалуйста, обновите свой тарифный план , чтобы иметь возможность добавлять больше пользователей.' - warning_user_limit_reached: > - Добавление дополнительных пользователей превысит текущий лимит. Пожалуйста, свяжитесь с администратором, чтобы увеличить лимит пользователей, чтобы убедиться, что внешние пользователи имеют доступ к этому пакету работ. - warning_user_limit_reached_admin: > - Добавление дополнительных пользователей превысит текущий лимит. Пожалуйста, обновите свой план , чтобы обеспечить доступ внешних пользователей к этому рабочему пакету. - warning_no_selected_user: "Пожалуйста, выберите пользователей для отправки этого пакета работ" - warning_locked_user: "Пользователь %{user} заблокирован, и им нельзя поделиться с другими пользователями." - user_details: - locked: "Заблокированный пользователь" - invited: "Приглашение отправлено. " - resend_invite: "Переслать" - invite_resent: "Приглашение отправлено повторно" - not_project_member: "Не участник проекта" - project_group: "Члены группы могут иметь дополнительные привилегии (как участники проекта)" - not_project_group: "Группа (совместно со всеми участниками)" - additional_privileges_project: "Есть дополнительные привилегии (в качестве участника проекта)" - additional_privileges_group: "Есть дополнительные привилегии (в качестве члена группы)" - additional_privileges_project_or_group: "Есть дополнительные привилегии (в качестве участника проекта или группы)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Невыбранные дни пропускаются при планировании пакетов работ (и не включаются в подсчет дней). Они могут быть переопределены на уровне пакетов работ. diff --git a/config/locales/crowdin/rw.yml b/config/locales/crowdin/rw.yml index df4b8e1e90af..f595671f38e4 100644 --- a/config/locales/crowdin/rw.yml +++ b/config/locales/crowdin/rw.yml @@ -516,6 +516,10 @@ rw: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ rw: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ rw: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3359,54 +3368,56 @@ rw: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Role" - type: "Type" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Comment" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Edit" - edit_description: "Can view, comment and edit this work package." - view: "View" - view_description: "Can view this work package." - remove: "Remove" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/si.yml b/config/locales/crowdin/si.yml index 66c7a899427d..d0b4eefa84d7 100644 --- a/config/locales/crowdin/si.yml +++ b/config/locales/crowdin/si.yml @@ -516,6 +516,10 @@ si: move: no_common_statuses_exists: "තෝරාගත් සියලුම වැඩ පැකේජ සඳහා කිසිදු තත්වයක් නොමැත. ඔවුන්ගේ තත්වය වෙනස් කළ නොහැක." unsupported_for_multiple_projects: "බහු ව්යාපෘති වලින් වැඩ පැකේජ සඳහා තොග චලනය/පිටපතක් සහාය නොදක්වයි" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ si: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ si: notice_principals_found_multiple: "සොයාගෙන ඇත %{number} ප්රතිඵල. \n පළමු ප්රතිඵලය අවධානය යොමු කිරීමට ටැබ්." notice_principals_found_single: "එක් ප්රති result ලයක් තිබේ. \n එය අවධානය යොමු කිරීමට ටැබ්." notice_project_not_deleted: "ව්යාපෘතිය මකා දැමුවේ නැත." + notice_project_not_found: "Project not found." notice_successful_connection: "සාර්ථක සම්බන්ධතාවයක්." notice_successful_create: "සාර්ථක නිර්මාණය." notice_successful_delete: "සාර්ථක මකාදැමීම." @@ -3359,54 +3368,56 @@ si: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "කාර්යභාරය" - type: "වර්ගය" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "අදහස් දක්වන්න" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "සංස්කරණය කරන්න" - edit_description: "Can view, comment and edit this work package." - view: "දැක්ම" - view_description: "Can view this work package." - remove: "ඉවත් කරන්න" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "බෙදාගෙන නැත" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/sk.yml b/config/locales/crowdin/sk.yml index e40123393b89..7bfac19d4f82 100644 --- a/config/locales/crowdin/sk.yml +++ b/config/locales/crowdin/sk.yml @@ -530,6 +530,10 @@ sk: move: no_common_statuses_exists: "Pre všetky vybraté pracovné balíčky nie je k dispozícii žiadny stav. Ich stav nemožno zmeniť." unsupported_for_multiple_projects: "Hromadné premiestnenie alebo kopírovanie nie je podporované pre pracovné balíky z viacerých projektov" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -929,6 +933,10 @@ sk: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2664,6 +2672,7 @@ sk: notice_principals_found_multiple: "Nájdených %{number} výsledkov. \nStlačte TAB pre prechod na prvý výsledok." notice_principals_found_single: "Bol nájdený jeden výsledok.\nStlačením tlačidla TAB zobraziť." notice_project_not_deleted: "Projekt nebol odstránený." + notice_project_not_found: "Project not found." notice_successful_connection: "Úspešne pripojené." notice_successful_create: "Úspešne vytvorené." notice_successful_delete: "Úspešne zmazané." @@ -3430,54 +3439,56 @@ sk: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Rola" - type: "Typ" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Komentár" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Upraviť" - edit_description: "Can view, comment and edit this work package." - view: "Zobraziť" - view_description: "Can view this work package." - remove: "Vymazať" - share: "Zdieľať" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Nezdieľané" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/sl.yml b/config/locales/crowdin/sl.yml index 87f54e832452..f75b8ffdf81a 100644 --- a/config/locales/crowdin/sl.yml +++ b/config/locales/crowdin/sl.yml @@ -527,6 +527,10 @@ sl: move: no_common_statuses_exists: "Za vse izbrane delovne pakete ni na voljo nobenega statusa. Njihovega statusa ni mogoče spremeniti." unsupported_for_multiple_projects: "Prenašanje / kopiranje v velikem obsegu ni podprto za delovne pakete iz več projektov" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -926,6 +930,10 @@ sl: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2662,6 +2670,7 @@ sl: notice_principals_found_multiple: "Najdeno je %{number} rezultatov.\nKliknite tipko tab da fokusirate prvi rezultat." notice_principals_found_single: "Najden je en rezultat.\nPritisnite tab, da ga prikaže." notice_project_not_deleted: "Projekt ni bil izbrisan." + notice_project_not_found: "Project not found." notice_successful_connection: "Povezava uspela." notice_successful_create: "Ustvarjanje uspelo." notice_successful_delete: "Uspešen izbris." @@ -3427,54 +3436,56 @@ sl: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Vloga" - type: "Vrsta" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Komentar" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Uredi" - edit_description: "Can view, comment and edit this work package." - view: "Ogled" - view_description: "Can view this work package." - remove: "Odstrani" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Ni deljeno" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/sr.yml b/config/locales/crowdin/sr.yml index 9aa14c88298e..7f185552e927 100644 --- a/config/locales/crowdin/sr.yml +++ b/config/locales/crowdin/sr.yml @@ -523,6 +523,10 @@ sr: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -922,6 +926,10 @@ sr: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2631,6 +2639,7 @@ sr: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3395,54 +3404,56 @@ sr: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Role" - type: "Type" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Comment" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Edit" - edit_description: "Can view, comment and edit this work package." - view: "View" - view_description: "Can view this work package." - remove: "Remove" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/sv.yml b/config/locales/crowdin/sv.yml index 627fbac630fd..4d9f3f216d2d 100644 --- a/config/locales/crowdin/sv.yml +++ b/config/locales/crowdin/sv.yml @@ -515,6 +515,10 @@ sv: move: no_common_statuses_exists: "Det finns ingen status för alla valda arbetspaket. Deras status kan inte ändras." unsupported_for_multiple_projects: "Att flytta/kopiera i bulk stöds inte för arbetspaket från flera projekt" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -914,6 +918,10 @@ sv: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2596,6 +2604,7 @@ sv: notice_principals_found_multiple: "%{number} resultat hittades. Använd TAB för att fokusera på det första." notice_principals_found_single: "Ett resultat. Använd TAB för att fokusera på det." notice_project_not_deleted: "Projektet raderades ej." + notice_project_not_found: "Project not found." notice_successful_connection: "Lyckad anslutning." notice_successful_create: "Skapades utan problem." notice_successful_delete: "Raderades utan problem." @@ -3355,54 +3364,56 @@ sv: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 användare" - one: "1 användare" - other: "%{count} användare" - filter: - project_member: "Projektmedlem" - not_project_member: "Not project member" - project_group: "Projektgrupp" - not_project_group: "Not project group" - role: "Roll" - type: "Typ" - label_search: "Search for users to invite" - label_search_placeholder: "Sök efter användare eller e-postadress" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Kommentar" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Redigera" - edit_description: "Can view, comment and edit this work package." - view: "Vy" - view_description: "Can view this work package." - remove: "Ta bort" - share: "Dela" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Inte delade" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/th.yml b/config/locales/crowdin/th.yml index b5c02f603b63..8841d1415f8b 100644 --- a/config/locales/crowdin/th.yml +++ b/config/locales/crowdin/th.yml @@ -509,6 +509,10 @@ th: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -908,6 +912,10 @@ th: enabled_modules: dependency_missing: "ต้องเปิดใช้งานโมดูล '%{dependency}' ด้วย เนื่องจากโมดูล '%{module}' ขึ้นอยู่กับโมดูลนั้น" format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2563,6 +2571,7 @@ th: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "ไม่ได้ลบโครงการ" + notice_project_not_found: "Project not found." notice_successful_connection: "การเชื่อมต่อสำเร็จ" notice_successful_create: "สร้างเรียบร้อยแล้ว" notice_successful_delete: "ลบเรียบร้อยแล้ว" @@ -3323,54 +3332,56 @@ th: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "บทบาท" - type: "ประเภท" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "ความคิดเห็น" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "แก้ไข" - edit_description: "Can view, comment and edit this work package." - view: "ดู" - view_description: "Can view this work package." - remove: "Remove" - share: "แชร์" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "ไม่ได้แบ่งปัน" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/tr.yml b/config/locales/crowdin/tr.yml index 30ede2b66cec..723f266e0c8e 100644 --- a/config/locales/crowdin/tr.yml +++ b/config/locales/crowdin/tr.yml @@ -515,6 +515,10 @@ tr: move: no_common_statuses_exists: "Seçilen tüm iş paketleri için uygun bir durum yoktur. Durumları değiştirilemez." unsupported_for_multiple_projects: "Birden çok projeden iş paketi için toplu taşıma / kopyalama desteklenmiyor" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -914,6 +918,10 @@ tr: enabled_modules: dependency_missing: "'%{module}' modülü buna bağlı olduğundan, '%{dependency}' modülünün de etkinleştirilmesi gerekiyor." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2595,6 +2603,7 @@ tr: notice_principals_found_multiple: "%{number} sonuç bulundu.\nİlk sonuç için TABa basın." notice_principals_found_single: "Bir sonuç bulundu. \n Ona gitmek için TABa basın." notice_project_not_deleted: "Proje silinemedi." + notice_project_not_found: "Project not found." notice_successful_connection: "Bağlantı başarılı." notice_successful_create: "Oluşturma başarılı." notice_successful_delete: "Silme başarılı." @@ -3354,54 +3363,56 @@ tr: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Rol" - type: "Tür" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Yorum" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Düzenle" - edit_description: "Can view, comment and edit this work package." - view: "Göster" - view_description: "Can view this work package." - remove: "Kaldır" - share: "Paylaş" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "Herhangi bir eşleşme bulunamadı." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Paylaşılmamış" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "%{user} kullanıcısı kilitli ve paylaşılamaz" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/uk.yml b/config/locales/crowdin/uk.yml index ce0ac4edeeac..366a6a9ecb5b 100644 --- a/config/locales/crowdin/uk.yml +++ b/config/locales/crowdin/uk.yml @@ -524,6 +524,10 @@ uk: move: no_common_statuses_exists: "Немає доступного статусу для всіх вибраних робочих пакетів. Їх статус не може бути змінений." unsupported_for_multiple_projects: "Масове переміщення/копіювання не підтримується для робочих пакетів з декількох проектів" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Відсутній робочий процес для надання доступу до пакета робіт" @@ -923,6 +927,10 @@ uk: enabled_modules: dependency_missing: "Модуль «%{dependency}» потрібно також увімкнути, оскільки від нього залежить модуль «%{module}»." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2658,6 +2666,7 @@ uk: notice_principals_found_multiple: "There are %{number} знайдені результати. \nВкладка для фокусування першого результату." notice_principals_found_single: "Знайдений один результат. Натисніть для переходу до нього." notice_project_not_deleted: "Проект не був видалений." + notice_project_not_found: "Project not found." notice_successful_connection: "Підключення успішно встановлене." notice_successful_create: "Створення успішно завершене." notice_successful_delete: "Видалення успішно завершене." @@ -3421,54 +3430,56 @@ uk: work_based_help_text: "Значення параметра «% завершення» автоматично виводиться зі значень параметрів «Робота» й «Залишок роботи»." status_based_help_text: "Значення параметра «% завершення» визначається статусом пакета робіт." migration_warning_text: "У режимі обчислення прогресу на основі робіт значення параметра «% завершення» не можна встановити вручну й прив’язати до значення параметра «Робота». Наявне значення збережено, але його не можна змінити. Спочатку визначте параметр «Робота»." - sharing: - count: - zero: "0 користувачів" - one: "1 користувач" - other: "Користувачів: %{count}" - filter: - project_member: "Учасник проєкту" - not_project_member: "Не учасник проєкту" - project_group: "Група проєкту" - not_project_group: "Не група проєкту" - role: "роль" - type: "Тип" - label_search: "Шукати й запрошувати користувачів" - label_search_placeholder: "Пошук за користувачем або електронною адресою" - label_toggle_all: "Перемкнути всі спільні ресурси" - permissions: - comment: "Коментар" - comment_description: "Може переглядати й коментувати цей пакет робіт." - denied: "У вас немає дозволу надавати доступ до пакетів робіт." - edit: "Редагувати" - edit_description: "Може переглядати, коментувати й редагувати цей пакет робіт." - view: "Перегляд" - view_description: "Можна переглядати цей пакет робіт." - remove: "Видалити" - share: "Поширити" - text_empty_search_description: "Користувачів за поточними умовами фільтра не знайдено" - text_empty_search_header: "Результатів за запитом не знайдено." - text_empty_state_description: "Доступ до цього пакета робіт ще нікому не надано." - text_empty_state_header: "Немає доступу" - text_user_limit_reached: "Додавання користувачів призведе до перевищення поточного ліміту. Зверніться до адміністратора, щоб збільшити ліміт користувачів і таким чином забезпечити доступ до цього пакета робіт зовнішнім користувачам." - text_user_limit_reached_admins: 'Додавання користувачів призведе до перевищення поточного ліміту. Підвищте рівень свого плану, щоб додати користувачів.' - warning_user_limit_reached: > - Додавання користувачів призведе до перевищення поточного ліміту. Зверніться до адміністратора, щоб збільшити ліміт користувачів і таким чином забезпечити доступ до цього пакета робіт зовнішнім користувачам. - warning_user_limit_reached_admin: > - Додавання користувачів призведе до перевищення поточного ліміту. Підвищте рівень свого плану, щоб забезпечити доступ до цього пакета робіт зовнішнім користувачам. - warning_no_selected_user: "Виберіть користувачів, яким потрібно надати доступ до цього пакета робіт" - warning_locked_user: "Користувача %{user} заблоковано, і йому не можна надати доступ" - user_details: - locked: "Заблокований користувач" - invited: "Запрошення надіслано. " - resend_invite: "Переслати." - invite_resent: "Запрошення надіслано повторно" - not_project_member: "Не учасник проєкту" - project_group: "Учасники групи можуть мати додаткові повноваження (як учасники проєкту)" - not_project_group: "Група (спільна для всіх учасників)" - additional_privileges_project: "Може мати додаткові повноваження (як учасник проєкту)" - additional_privileges_group: "Може мати додаткові повноваження (як учасник групи)" - additional_privileges_project_or_group: "Може мати додаткові повноваження (як учасник проєкту або групи)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Під час планування пакетів робіт невибрані дні пропускаються й не включаються в кількість днів. Це налаштування можна змінити на рівні пакета робіт. diff --git a/config/locales/crowdin/uz.yml b/config/locales/crowdin/uz.yml index 006fdc25ff53..ba4214f6fb58 100644 --- a/config/locales/crowdin/uz.yml +++ b/config/locales/crowdin/uz.yml @@ -516,6 +516,10 @@ uz: move: no_common_statuses_exists: "There is no status available for all selected work packages. Their status cannot be changed." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -915,6 +919,10 @@ uz: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2597,6 +2605,7 @@ uz: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3359,54 +3368,56 @@ uz: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Role" - type: "Type" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Comment" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Edit" - edit_description: "Can view, comment and edit this work package." - view: "View" - view_description: "Can view this work package." - remove: "Remove" - share: "Share" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Not shared" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/vi.yml b/config/locales/crowdin/vi.yml index 62e5db51e4c0..14f2e15996e7 100644 --- a/config/locales/crowdin/vi.yml +++ b/config/locales/crowdin/vi.yml @@ -511,6 +511,10 @@ vi: move: no_common_statuses_exists: "Có là tình trạng không có sẵn cho tất cả các gói đã chọn công việc. Tình trạng của họ không thể thay đổi." unsupported_for_multiple_projects: "Bulk move/copy is not supported for work packages from multiple projects" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -910,6 +914,10 @@ vi: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2565,6 +2573,7 @@ vi: notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." notice_project_not_deleted: "The project wasn't deleted." + notice_project_not_found: "Project not found." notice_successful_connection: "Successful connection." notice_successful_create: "Successful creation." notice_successful_delete: "Successful deletion." @@ -3324,54 +3333,56 @@ vi: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." - sharing: - count: - zero: "0 users" - one: "1 user" - other: "%{count} users" - filter: - project_member: "Project member" - not_project_member: "Not project member" - project_group: "Project group" - not_project_group: "Not project group" - role: "Vai trò" - type: "Kiểu" - label_search: "Search for users to invite" - label_search_placeholder: "Search by user or email address" - label_toggle_all: "Toggle all shares" - permissions: - comment: "Nhận xét" - comment_description: "Can view and comment this work package." - denied: "You don't have permissions to share work packages." - edit: "Chỉnh sửa" - edit_description: "Can view, comment and edit this work package." - view: "Khung nhìn" - view_description: "Can view this work package." - remove: "Xoá" - share: "Chia sẻ" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "The work package has not been shared with anyone yet." - text_empty_state_header: "Không được chia sẻ" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/crowdin/zh-CN.yml b/config/locales/crowdin/zh-CN.yml index d4f8b6336ab7..9bbd91bcebbb 100644 --- a/config/locales/crowdin/zh-CN.yml +++ b/config/locales/crowdin/zh-CN.yml @@ -505,6 +505,10 @@ zh-CN: move: no_common_statuses_exists: "所有选中的工作包都没有可用的状态,因此无法更改其状态。" unsupported_for_multiple_projects: "不支持多个项目工作包的大批量移动/复制" + current_type_not_available_in_target_project: > + 当前工作包类型未在目标项目中启用。 如果您希望目标项目保持不变,请启用此类型。 否则,工作包的类型将被自动重新分配,导致可能的数据丢失。 + bulk_current_type_not_available_in_target_project: > + 当前的工作包类型未在目标项目中启用。 如果您希望目标项目中的类型保持不变,请启用这些类型。 否则,工作包类型将被自动重新分配,导致可能的数据丢失。 sharing: missing_workflow_warning: title: "工作包共享缺少工作流" @@ -904,6 +908,10 @@ zh-CN: enabled_modules: dependency_missing: "还需要启用模块“%{dependency}”,因为模块“%{module}”依赖于该模块。" format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -2557,6 +2565,7 @@ zh-CN: notice_principals_found_multiple: "找到 %{number} 条结果。\n 点击以关注第一条结果。" notice_principals_found_single: "这有一个结果。\n点击来关注它。" notice_project_not_deleted: "项目没有被删除" + notice_project_not_found: "Project not found." notice_successful_connection: "成功连接。" notice_successful_create: "成功创建。" notice_successful_delete: "成功删除。" @@ -3312,54 +3321,56 @@ zh-CN: work_based_help_text: "完成百分比由 \"工时\" 和 \"剩余工时\" 自动得出。" status_based_help_text: "完成百分比由工作包状态设定。" migration_warning_text: "在基于工时的进度计算模式下,完成百分比不能手动设置,而是与工时绑定。现有值已被保留,但无法编辑。请先输入工时。" - sharing: - count: - zero: "0 个用户" - one: "1 个用户" - other: "%{count} 个用户" - filter: - project_member: "项目成员" - not_project_member: "非项目成员" - project_group: "项目组" - not_project_group: "非项目组" - role: "角色" - type: "类型" - label_search: "搜索要邀请的用户" - label_search_placeholder: "按用户名或电子邮件地址搜索" - label_toggle_all: "切换所有共享" - permissions: - comment: "评论" - comment_description: "可以查看和评论此工作包。" - denied: "您没有共享工作包的权限。" - edit: "编辑" - edit_description: "可以查看、评论和编辑此工作包。" - view: "查看" - view_description: "可以查看此工作包。" - remove: "移除" - share: "分享" - text_empty_search_description: "没有符合当前过滤条件的用户。" - text_empty_search_header: "我们找不到任何匹配的结果。" - text_empty_state_description: "工作包尚未共享给任何人。" - text_empty_state_header: "未共享" - text_user_limit_reached: "添加额外的用户将超出当前限制。请联系管理员以增加用户限制,以确保外部用户能够访问此工作包。" - text_user_limit_reached_admins: '添加额外的用户将超出当前限制。请升级您的计划以添加更多用户。' - warning_user_limit_reached: > - 添加额外的用户将超出当前限制。请联系管理员以增加用户限制,以确保外部用户能够访问此工作包。 - warning_user_limit_reached_admin: > - 添加额外的用户将超出当前限制。请升级您的计划,以确保外部用户能够访问此工作包。 - warning_no_selected_user: "请选择要共享此工作包的用户" - warning_locked_user: "用户 %{user} 已被锁定且无法被分享" - user_details: - locked: "锁定的用户" - invited: "邀请已发送。 " - resend_invite: "重新发送。" - invite_resent: "邀请已重新发送" - not_project_member: "非项目成员" - project_group: "组成员可能具有额外的权限(作为项目成员)" - not_project_group: "组 (与所有成员共享)" - additional_privileges_project: "可能有额外的权限(作为项目成员)" - additional_privileges_group: "可能有额外的权限(作为群组成员)" - additional_privileges_project_or_group: "可能有额外的权限(作为项目成员或组成员)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > 在安排工作包时,未选择的日期将被跳过(且不计入天数)。可以在工作包级别覆盖这些选项。 diff --git a/config/locales/crowdin/zh-TW.yml b/config/locales/crowdin/zh-TW.yml index 8d20f56df62b..41e060e8328a 100644 --- a/config/locales/crowdin/zh-TW.yml +++ b/config/locales/crowdin/zh-TW.yml @@ -26,13 +26,13 @@ zh-TW: no_results_title_text: 在這一時限內, 該專案沒有任何活動。 admin: plugins: - no_results_title_text: 現主時沒有安裝任何外掛程式 + no_results_title_text: 目前沒有安裝任何外掛程式 no_results_content_text: See our integrations and plugins page for more information. custom_styles: color_theme: "色彩佈景主題" color_theme_custom: "自訂" colors: - primary-button-color: "Primary button" + primary-button-color: "主按鈕" accent-color: "強調(Accent)" header-bg-color: "頁首背景" header-item-bg-hover-color: "頁首背景(滑鼠停留)" @@ -501,13 +501,17 @@ zh-TW: copy_failed: "此工作項目無法被複製" move_failed: "此工作項目無法被移動" could_not_be_saved: "以下文檔無法被保存" - none_could_be_saved: " %{total} 工作項目無法更新" + none_could_be_saved: " %{total} 個工作項目無法更新" x_out_of_y_could_be_saved: "%{failing} out of the %{total} work packages could not be updated while %{success} could." selected_because_descendants: "While %{selected} work packages where selected, in total %{total} work packages are affected which includes descendants." descendant: "descendant of selected" move: no_common_statuses_exists: "被選取的工作項目沒有可用的狀態。他們的狀態不可以變更。" unsupported_for_multiple_projects: "工作項目不支援從多個專案的大區塊的移動/複製" + current_type_not_available_in_target_project: > + The current type of the work package is not enabled in the target project. Please enable the type in the target project if you'd like them to remain unchanged. Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. Please enable the types in the target project if you'd like them to remain unchanged. Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: title: "Workflow missing for work package sharing" @@ -595,7 +599,7 @@ zh-TW: custom_action: actions: "操作" custom_field: - allow_non_open_versions: "允許非開放版本" + allow_non_open_versions: "允許非打開版本" default_value: "預設值" editable: "可編輯" field_format: "格式" @@ -644,7 +648,7 @@ zh-TW: public_value: title: "可見度" true: "公開" - false: "不公開" + false: "私人的" queries: "查詢" status_code: "專案狀態" description: "說明" @@ -907,6 +911,10 @@ zh-TW: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + project_custom_field_project_mapping: + attributes: + project_ids: + blank: "Please select a project." query: attributes: project: @@ -1081,7 +1089,7 @@ zh-TW: unassignable: "無法分派到專案。" version: undeletable_archived_projects: "該版本無法刪除,因為包含工作項目。" - undeletable_work_packages_attached: "該版本無法刪除,因為包含工作項目。" + undeletable_work_packages_attached: "該版本無法刪除,因為它附帶了工作項目。" status: readonly_default_exlusive: ",是無法生效的(因其設定為「預設」狀態)" template: @@ -2124,7 +2132,7 @@ zh-TW: label_privacy_policy: "資料隱私與安全性政策" label_product_version: "產品版本" label_profile: "個人資料" - label_percent_complete: "完成度(%)" + label_percent_complete: "(%)完成" label_project_activity: "專案活動" label_project_attribute_plural: "專案屬性" label_project_attribute_manage_link: "Manage project attributes" @@ -2135,7 +2143,7 @@ zh-TW: label_project_hierarchy: "專案結構" label_project_new: "新增專案" label_project_plural: "專案" - label_project_list_plural: "列出專案" + label_project_list_plural: "專案列表" label_project_attributes_plural: "專案屬性" label_project_custom_field_plural: "專案屬性" label_project_settings: "專案設定" @@ -2162,7 +2170,7 @@ zh-TW: label_relation_delete: "刪除關聯" label_relation_new: "新增關聯" label_release_notes: "發行備註" - label_remaining_work: "剩餘工時" + label_remaining_work: "剩餘工作" label_remove_columns: "移除所選欄" label_renamed: "重新命名" label_reply_plural: "回覆" @@ -2328,7 +2336,7 @@ zh-TW: other: "%{count} 個未完成" zero: "0 個已開啟" label_x_work_packages: - one: "一個工作項目" + one: "1個工作項目" other: "%{count} 個工作項目" zero: "無工作項目" label_x_projects: @@ -2561,6 +2569,7 @@ zh-TW: notice_principals_found_multiple: "找到 %{number} 條結果。\n Tab鍵以轉到第一條結果。" notice_principals_found_single: "有一個結果。 \n Tab鍵轉到該結果。" notice_project_not_deleted: "專案沒有被刪除" + notice_project_not_found: "Project not found." notice_successful_connection: "連接成功" notice_successful_create: "建立成功" notice_successful_delete: "刪除成功" @@ -3321,54 +3330,56 @@ zh-TW: work_based_help_text: "% Complete is automatically derived from Work and Remaining work." status_based_help_text: "% Complete is set by work package status." migration_warning_text: "在「Work-based」進度計算模式下,完成百分比無法手動設置,並且與「Work」相關聯。目前手動輸入數值已保留,無法編輯。 請務必輸入「Work」才能進行。" - sharing: - count: - zero: "0個用戶" - one: "1 個用戶" - other: "%{count} 個使用者" - filter: - project_member: "專案成員" - not_project_member: "非專案的成員" - project_group: "專案群組" - not_project_group: "排除專案群組" - role: "角色" - type: "類型" - label_search: "搜尋要邀請的用戶" - label_search_placeholder: "以帳號或電子郵件搜尋" - label_toggle_all: "切換到「所有分享的」" - permissions: - comment: "留言" - comment_description: "可以查看與留言此工作項目" - denied: "你沒有分享工作項目的權限" - edit: "編輯" - edit_description: "可以查看,留言與編輯此工作項目" - view: "檢視" - view_description: "查看此工作項目" - remove: "刪除" - share: "共享" - text_empty_search_description: "There are no users with the current filter criteria." - text_empty_search_header: "We couldn't find any matching results." - text_empty_state_description: "此工作項目尚未與任何人分享" - text_empty_state_header: "非共享的" - text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package." - text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "請選擇使用者與此工作項目共享" - warning_locked_user: "使用者 %{user} 已停用,無法共享" - user_details: - locked: "使用者已停用" - invited: "已傳送邀請" - resend_invite: "重新傳送" - invite_resent: "已重傳邀請" - not_project_member: "非專案的成員" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "成員共享群組" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overridden at a work-package level. diff --git a/config/locales/en.yml b/config/locales/en.yml index bd92d124be41..913f69703039 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -578,6 +578,14 @@ Project attributes and sections are defined in the + The current type of the work package is not enabled in the target project. + Please enable the type in the target project if you'd like them to remain unchanged. + Otherwise, the work package's type will be automatically re-assigned leading to potential data loss. + bulk_current_type_not_available_in_target_project: > + The current types of the work packages aren't enabled in the target project. + Please enable the types in the target project if you'd like them to remain unchanged. + Otherwise, the work packages' types will be automatically re-assigned leading to potential data loss. sharing: missing_workflow_warning: @@ -987,6 +995,10 @@ Project attributes and sections are defined in the upgrade your plan to be able to add more users.' - warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package. - warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this work package. - warning_no_selected_user: "Please select users to share this work package with" - warning_locked_user: "The user %{user} is locked and cannot be shared with" - user_details: - locked: "Locked user" - invited: "Invite sent. " - resend_invite: "Resend." - invite_resent: "Invite has been resent" - not_project_member: "Not a project member" - project_group: "Group members might have additional privileges (as project members)" - not_project_group: "Group (shared with all members)" - additional_privileges_project: "Might have additional privileges (as project member)" - additional_privileges_group: "Might have additional privileges (as group member)" - additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" + permissions: + comment: "Comment" + comment_description: "Can view and comment this work package." + edit: "Edit" + edit_description: "Can view, comment and edit this work package." + view: "View" + view_description: "Can view this work package." + sharing: + count: + zero: "0 users" + one: "1 user" + other: "%{count} users" + filter: + project_member: "Project member" + not_project_member: "Not project member" + project_group: "Project group" + not_project_group: "Not project group" + user: "User" + group: "Group" + role: "Role" + type: "Type" + denied: "You don't have permissions to share %{entities}." + label_search: "Search for users to invite" + label_search_placeholder: "Search by user or email address" + label_toggle_all: "Toggle all shares" + remove: "Remove" + share: "Share" + text_empty_search_description: "There are no users with the current filter criteria." + text_empty_search_header: "We couldn't find any matching results." + text_empty_state_description: "The %{entity} has not been shared with anyone yet." + text_empty_state_header: "Not shared" + text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." + text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' + warning_user_limit_reached: > + Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + warning_user_limit_reached_admin: > + Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + warning_no_selected_user: "Please select users to share this %{entity} with" + warning_locked_user: "The user %{user} is locked and cannot be shared with" + user_details: + locked: "Locked user" + invited: "Invite sent. " + resend_invite: "Resend." + invite_resent: "Invite has been resent" + not_project_member: "Not a project member" + project_group: "Group members might have additional privileges (as project members)" + not_project_group: "Group (shared with all members)" + additional_privileges_project: "Might have additional privileges (as project member)" + additional_privileges_group: "Might have additional privileges (as group member)" + additional_privileges_project_or_group: "Might have additional privileges (as project or group member)" working_days: info: > diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 25ada1922c27..3ab5a7eaa6d9 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -172,6 +172,7 @@ en: attribute_reference: macro_help_tooltip: "This text segment is being dynamically rendered by a macro." not_found: "Requested resource could not be found" + nested_macro: "This macro is recursively referencing %{model} %{id}." invalid_attribute: "The selected attribute '%{name}' does not exist." child_pages: button: "Links to child pages" @@ -989,6 +990,11 @@ en: preformatted_text: "Preformatted Text" wiki_link: "Link to a Wiki page" image: "Image" + sharing: + share: "Share" + selected_count: "%{count} selected" + selection: + mixed: "Mixed" work_packages: bulk_actions: move: "Bulk change of project" @@ -1145,12 +1151,8 @@ en: is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." sharing: - share: "Share" title: "Share work package" show_all_users: "Show all users with whom the work package has been shared with" - selected_count: "%{count} selected" - selection: - mixed: "Mixed" upsale: description: "Share work packages with users who are not members of the project." table: diff --git a/config/routes.rb b/config/routes.rb index 8c273c0506c0..10d63e1decb6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -76,6 +76,22 @@ via: :all end + # Shared route concerns + # TODO: Add description how to configure controller to support shares + concern :shareable do + resources :members, path: :shares, controller: "shares", only: %i[index create update destroy] do + member do + post "resend_invite" => "shares#resend_invite" + end + + collection do + patch :bulk, to: "shares#bulk_update" + put :bulk, to: "shares#bulk_update" + delete :bulk, to: "shares#bulk_destroy" + end + end + end + scope controller: "account" do get "/account/force_password_change", action: "force_password_change" post "/account/change_password", action: "change_password" @@ -179,18 +195,19 @@ delete "/favorite" => "favorites#unfavorite" end - namespace :projects do - resource :menu, only: %i[show] - resources :queries, only: %i[show new create update destroy] do - member do - get :rename + resources :project_queries, only: %i[show new create update destroy], controller: "projects/queries" do + member do + get :rename - post :publish - post :unpublish - end + post :publish + post :unpublish end end + namespace :projects do + resource :menu, only: %i[show] + end + resources :projects, except: %i[show edit create update] do scope module: "projects" do namespace "settings" do @@ -466,6 +483,7 @@ put :drop get :project_mappings + get :new_link post :link delete :unlink end @@ -519,7 +537,7 @@ get "/bulk" => "bulk#destroy" end - resources :work_packages, only: [:index] do + resources :work_packages, only: [:index], concerns: [:shareable] do # move bulk of wps get "move/new" => "work_packages/moves#new", on: :collection, as: "new_move" post "move" => "work_packages/moves#create", on: :collection, as: "move" @@ -529,16 +547,6 @@ # states managed by client-side routing on work_package#index get "details/*state" => "work_packages#index", on: :collection, as: :details - # Rails managed sharing route - resources :members, path: :shares, controller: "work_packages/shares", only: %i[index create update destroy] do - member do - post "resend_invite" => "work_packages/shares#resend_invite" - end - collection do - resource :bulk, controller: "work_packages/shares/bulk", only: %i[update destroy], as: :shares_bulk - end - end - resource :progress, only: %i[new edit update], controller: "work_packages/progress" collection do resource :progress, @@ -552,7 +560,7 @@ get "/create_new" => "work_packages#index", on: :collection, as: "new_split" get "/new" => "work_packages#index", on: :collection, as: "new", state: "new" # We do not want to match the work package export routes - get "(/*state)" => "work_packages#show", on: :member, as: "", constraints: { id: /\d+/ } + get "(/*state)" => "work_packages#show", on: :member, as: "", constraints: { id: /\d+/, state: /(?!shares).+/ } get "/share_upsale" => "work_packages#index", on: :collection, as: "share_upsale" get "/edit" => "work_packages#show", on: :member, as: "edit" end diff --git a/db/migrate/20221115082403_add_ldap_tls_options.rb b/db/migrate/20221115082403_add_ldap_tls_options.rb index ef5dbb5d6c2e..862b5305e46c 100644 --- a/db/migrate/20221115082403_add_ldap_tls_options.rb +++ b/db/migrate/20221115082403_add_ldap_tls_options.rb @@ -42,7 +42,7 @@ def change # Current LDAP library default is to not verify the certificate MigratingAuthSource.reset_column_information - ldap_settings = Setting.find_by(name: 'ldap_tls_options')&.value + ldap_settings = Setting.find_by(name: "ldap_tls_options")&.value migrate_ldap_settings(ldap_settings) end end @@ -54,7 +54,7 @@ def migrate_ldap_settings(ldap_settings) return if ldap_settings.blank? parsed = Setting.deserialize_hash(ldap_settings) - verify_peer = parsed['verify_mode'] == OpenSSL::SSL::VERIFY_PEER + verify_peer = parsed["verify_mode"] == OpenSSL::SSL::VERIFY_PEER MigratingAuthSource.update_all(verify_peer:) rescue StandardError => e diff --git a/db/migrate/20240408132459_rename_delay_to_lag.rb b/db/migrate/20240408132459_rename_delay_to_lag.rb index 969c2eefbae5..386790aefb83 100644 --- a/db/migrate/20240408132459_rename_delay_to_lag.rb +++ b/db/migrate/20240408132459_rename_delay_to_lag.rb @@ -3,6 +3,6 @@ def change rename_column :relations, :delay, :lag # TODO remove after 14.0 - add_column :relations, :delay, :virtual, type: :integer, as: 'lag', stored: true + add_column :relations, :delay, :virtual, type: :integer, as: "lag", stored: true end end diff --git a/db/migrate/20240410060041_remove_virtual_delay_column.rb b/db/migrate/20240410060041_remove_virtual_delay_column.rb index 69c75e4083dd..5fc36f3cd66a 100644 --- a/db/migrate/20240410060041_remove_virtual_delay_column.rb +++ b/db/migrate/20240410060041_remove_virtual_delay_column.rb @@ -1,5 +1,5 @@ class RemoveVirtualDelayColumn < ActiveRecord::Migration[7.1] def change - remove_column :relations, :delay, :virtual, type: :integer, as: 'lag', stored: true + remove_column :relations, :delay, :virtual, type: :integer, as: "lag", stored: true end end diff --git a/docs/api/apiv3/components/schemas/news_create_model.yml b/docs/api/apiv3/components/schemas/news_create_model.yml new file mode 100644 index 000000000000..1d29fa7e977a --- /dev/null +++ b/docs/api/apiv3/components/schemas/news_create_model.yml @@ -0,0 +1,42 @@ +# Schema: NewsCreateModel +--- +type: object +properties: + title: + type: string + description: The headline of the news + readOnly: true + summary: + type: string + description: A short summary + readOnly: true + description: + allOf: + - $ref: "./formattable.yml" + - description: The main body of the news with all the details + _links: + type: object + required: + - project + properties: + project: + allOf: + - "$ref": "./link.yml" + - description: |- + The project the news is situated in + + **Resource**: Project +example: + _type: News + title: asperiores possimus nam doloribus ab + summary: Celebrer spiculum colo viscus claustrum atque. Id nulla culpa sumptus. + Comparo crapula depopulo demonstro. + description: + raw: '**Videlicet deserunt aequitas cognatus**. Concedo quia est quia pariatur vorago + vallum. Calco autem atavus accusamus conscendo cornu ulterius. Tam patria ago + consectetur ventito sustineo nihil caecus. Supra officiis eos velociter somniculosus + tonsor qui. Suffragium aduro arguo angustus cogito quia tolero vulnus. Supplanto + sortitus cresco apud vestrum qui.' + _links: + project: + href: "/api/v3/projects/1" diff --git a/docs/api/apiv3/openapi-spec.yml b/docs/api/apiv3/openapi-spec.yml index a1112d228cd4..e0eb062cdf76 100644 --- a/docs/api/apiv3/openapi-spec.yml +++ b/docs/api/apiv3/openapi-spec.yml @@ -673,6 +673,8 @@ components: "$ref": "./components/schemas/membership_schema_model.yml" MembershipWriteModel: "$ref": "./components/schemas/membership_write_model.yml" + NewsCreateModel: + "$ref": "./components/schemas/news_create_model.yml" NewsModel: "$ref": "./components/schemas/news_model.yml" NonWorkingDayCollectionModel: diff --git a/docs/api/apiv3/paths/news.yml b/docs/api/apiv3/paths/news.yml index 9908ee74a426..bf362bafd833 100644 --- a/docs/api/apiv3/paths/news.yml +++ b/docs/api/apiv3/paths/news.yml @@ -148,3 +148,61 @@ get: also on the requesting user's permissions. operationId: List_News summary: List News + +post: + summary: Create News + operationId: create_news + tags: + - News + description: |- + Creates a news entry. Only administrators and users with "Manage news" permission in the given project are eligible. + When calling this endpoint the client provides a single object, containing at least the properties and links that are required, in the body. + requestBody: + content: + application/json: + schema: + $ref: '../components/schemas/news_create_model.yml' + responses: + '201': + content: + application/hal+json: + schema: + $ref: '../components/schemas/news_model.yml' + description: Created + '400': + $ref: "../components/responses/invalid_request_body.yml" + '403': + content: + application/hal+json: + schema: + $ref: '../components/schemas/error_response.yml' + example: + _type: Error + errorIdentifier: urn:openproject-org:api:v3:errors:MissingPermission + message: You are not allowed to create new news. + description: |- + Returned if the client does not have sufficient permissions. + + **Required permission:** Administrator, Manage news permission in the project + '406': + $ref: "../components/responses/missing_content_type.yml" + '415': + $ref: "../components/responses/unsupported_media_type.yml" + '422': + content: + application/hal+json: + schema: + $ref: '../components/schemas/error_response.yml' + example: + _embedded: + details: + attribute: title + _type: Error + errorIdentifier: urn:openproject-org:api:v3:errors:PropertyConstraintViolation + message: Title can't be blank. + description: |- + Returned if: + + * the client tries to modify a read-only property (`PropertyIsReadOnly`) + + * a constraint for a property was violated (`PropertyConstraintViolation`) diff --git a/docs/api/apiv3/paths/news_item.yml b/docs/api/apiv3/paths/news_item.yml index 018e15a1015d..94512dc9c79c 100644 --- a/docs/api/apiv3/paths/news_item.yml +++ b/docs/api/apiv3/paths/news_item.yml @@ -76,3 +76,125 @@ get: description: '' operationId: View_news summary: View news +patch: + summary: Update news + operationId: update_news + tags: + - News + description: |- + Updates the news's writable attributes. + When calling this endpoint the client provides a single object, containing the properties and links to be updated, in the body. + parameters: + - description: News id + example: '1' + in: path + name: id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '../components/schemas/news_model.yml' + responses: + '200': + content: + application/hal+json: + schema: + $ref: '../components/schemas/news_model.yml' + description: OK + '400': + $ref: "../components/responses/invalid_request_body.yml" + '403': + content: + application/hal+json: + schema: + $ref: '../components/schemas/error_response.yml' + example: + _type: Error + errorIdentifier: urn:openproject-org:api:v3:errors:MissingPermission + message: You are not allowed to update this news. + description: |- + Returned if the client does not have sufficient permissions. + + **Required permission:** Administrators, Manage news permission + '404': + content: + application/hal+json: + schema: + $ref: '../components/schemas/error_response.yml' + example: + _type: Error + errorIdentifier: urn:openproject-org:api:v3:errors:NotFound + message: The specified news does not exist or you do not have permission to view it. + description: |- + Returned if the news entry does not exist or if the API user does not have the necessary permissions to update it. + + **Required permission:** Administrators, Manage news permission + '406': + $ref: "../components/responses/missing_content_type.yml" + '415': + $ref: "../components/responses/unsupported_media_type.yml" + '422': + content: + application/hal+json: + schema: + $ref: '../components/schemas/error_response.yml' + example: + _embedded: + details: + attribute: title + _type: Error + errorIdentifier: urn:openproject-org:api:v3:errors:PropertyConstraintViolation + message: Title can't be blank. + description: |- + Returned if: + + * the client tries to modify a read-only property (`PropertyIsReadOnly`) + + * a constraint for a property was violated (`PropertyConstraintViolation`) +delete: + summary: Delete news + operationId: delete_news + description: Permanently deletes the specified news entry. + tags: + - News + parameters: + - description: News id + example: '1' + in: path + name: id + required: true + schema: + type: integer + responses: + '202': + description: |- + Returned if the news was deleted successfully. + + Note that the response body is empty as of now. In future versions of the API a body + *might* be returned, indicating the progress of deletion. + '403': + content: + application/hal+json: + schema: + $ref: '../components/schemas/error_response.yml' + example: + _type: Error + errorIdentifier: urn:openproject-org:api:v3:errors:MissingPermission + message: You are not allowed to delete this news entry + description: |- + Returned if the client does not have sufficient permissions. + + **Required permission:** Administrators and Manage news permission + '404': + content: + application/hal+json: + schema: + $ref: '../components/schemas/error_response.yml' + example: + _type: Error + errorIdentifier: urn:openproject-org:api:v3:errors:NotFound + message: The specified news does not exist. + description: Returned if the news does not exist. diff --git a/docs/development/contribution-documentation/documentation-style-guide/README.md b/docs/development/contribution-documentation/documentation-style-guide/README.md index 3f804689d083..66b075db7d37 100644 --- a/docs/development/contribution-documentation/documentation-style-guide/README.md +++ b/docs/development/contribution-documentation/documentation-style-guide/README.md @@ -381,5 +381,51 @@ At the moment it is not possible for external contributors to upload videos to t Use alert boxes to call attention to information. The alert boxes in the OpenProject documentation have a specific format. Please use the following to be consistent: ```markdown -> **Note**: If you do not have an OpenProject installation yet, please visit our site on [how to create an OpenProject trial installation](../../enterprise-guide/enterprise-cloud-guide/#create-a-new-account). +> **Note**: If you do not have an OpenProject installation yet, please visit our site on [how to create an OpenProject trial installation](../../../enterprise-guide/enterprise-cloud-guide/create-cloud-trial/). ``` + +> **Note**: If you do not have an OpenProject installation yet, please visit our site on [how to create an OpenProject trial installation](../../../enterprise-guide/enterprise-cloud-guide/create-cloud-trial/). + +## Alerts + +Alerts are a Markdown extension by Github based on the blockquote syntax that you can use to emphasize critical information. + +```markdown +> [!NOTE] +> Useful information that users should know, even when skimming content. + + +> [!TIP] +> Helpful advice for doing things better or more easily. + + +> [!IMPORTANT] +> Key information users need to know to achieve their goal. + + +> [!WARNING] +> Urgent info that needs immediate user attention to avoid problems. + + +> [!CAUTION] +> Advises about risks or negative outcomes of certain actions. +``` + +> [!NOTE] +> Useful information that users should know, even when skimming content. + + +> [!TIP] +> Helpful advice for doing things better or more easily. + + +> [!IMPORTANT] +> Key information users need to know to achieve their goal. + + +> [!WARNING] +> Urgent info that needs immediate user attention to avoid problems. + + +> [!CAUTION] +> Advises about risks or negative outcomes of certain actions. diff --git a/docs/development/development-environment-osx/README.md b/docs/development/development-environment-osx/README.md index 9c4c3509d4dc..0ea85c511bd7 100644 --- a/docs/development/development-environment-osx/README.md +++ b/docs/development/development-environment-osx/README.md @@ -216,9 +216,8 @@ RAILS_ENV=development bin/rails db:seed You can run all required workers of OpenProject through `overmind`, which combines them in a single tab. Optionally, you may also run `overmind` as a daemon and connect to services individually. -The `bin/dev` command will first check if `overmind` is available and run the application if via `Procfile.dev` if -possible. If not, -it falls back to `foreman`, installing it if needed. +The `bin/dev` command will first check if `overmind` is available and run the application via `Procfile.dev`. Otherwise +it will use `foreman` if it is available. ```shell bin/dev diff --git a/docs/development/development-environment-ubuntu/README.md b/docs/development/development-environment-ubuntu/README.md index 7a3c860abe5d..bf0117a93551 100644 --- a/docs/development/development-environment-ubuntu/README.md +++ b/docs/development/development-environment-ubuntu/README.md @@ -270,9 +270,8 @@ RAILS_ENV=development bin/rails db:seed You can run all required workers of OpenProject through `overmind`, which combines them in a single tab. Optionally, you may also run `overmind` as a daemon and connect to services individually. -The `bin/dev` command will first check if `overmind` is available and run the application if via `Procfile.dev` if -possible. If not, -it falls back to `foreman`, installing it if needed. +The `bin/dev` command will first check if `overmind` is available and run the application via `Procfile.dev`. Otherwise +it will use `foreman` if it is available. ```shell bin/dev diff --git a/docs/installation-and-operations/installation/docker/README.md b/docs/installation-and-operations/installation/docker/README.md index def0b32d2b2f..856da800a1de 100644 --- a/docs/installation-and-operations/installation/docker/README.md +++ b/docs/installation-and-operations/installation/docker/README.md @@ -173,7 +173,7 @@ docker run -d -p 8080:80 --name openproject \ ``` Please make sure you set the correct public facing hostname in `OPENPROJECT_HOST__NAME`. If you don't have a load-balancing or proxying web server in front of your docker container, -you will otherwise be vulnerable to [HOST header injections](https://portswigger.net/web-security/host-header), as the internal server has no way of identifying the correct host name. +you will otherwise be vulnerable to [HOST header injections](https://portswigger.net/web-security/host-header), as the internal server has no way of identifying the correct host name. We strongly recommend you use an external load-balancing or proxying web server for termination of TLS/SSL and general security hardening. **Note**: Make sure to replace `secret` with a random string. One way to generate one is to run `head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32 ; echo ''` if you are on Linux. diff --git a/docs/system-admin-guide/design/pdf-export-styles/README.md b/docs/system-admin-guide/design/pdf-export-styles/README.md index 0ab461f0659b..dce256aff5bd 100644 --- a/docs/system-admin-guide/design/pdf-export-styles/README.md +++ b/docs/system-admin-guide/design/pdf-export-styles/README.md @@ -15,6 +15,34 @@ This document describes the style settings format for the [PDF Export styling fi | `cover` | **Cover page**
      Styling for the cover page of the PDF report export
      See [Cover page](#cover-page) | object | | `overview` | **Overview**
      Styling for the PDF table export
      See [Overview](#overview) | object | +## Alert + +Styling to denote a quote as alert box + +Key: `alert` + +Example: + +```yaml +ALERT: + alert_color: f4f9ff + border_color: f4f9ff + border_width: 2 + no_border_right: true + no_border_left: false + no_border_bottom: true + no_border_top: true +``` + +| Key | Description | Data type | +| - | - | - | +| `background_color` | **Color**
      A color in RRGGBB format
      Example: `F0F0F0` | string | +| `alert_color` | **Color**
      A color in RRGGBB format
      Example: `F0F0F0` | string | +| … | See [Font properties](#font-properties) | | +| … | See [Border Properties](#border-properties) | | +| … | See [Padding Properties](#padding-properties) | | +| … | See [Margin properties](#margin-properties) | | + ## Border Properties Properties to set borders @@ -287,6 +315,7 @@ markdown: | `unordered_list_point` | **Markdown unordered list point**
      Default styling for unordered list points on all levels.
      use unordered_list_point_`x` as key for unordered list points level `x`.
      See [Markdown unordered list point](#markdown-unordered-list-point) | object | | `task_list` | **Markdown task list**
      See [Markdown unordered list](#markdown-unordered-list) | object | | `task_list_point` | **Markdown task list point**
      See [Markdown task list point](#markdown-task-list-point) | object | +| `alerts` | **alert boxes (styled blockquotes)**
      See [alert boxes (styled blockquotes)](#alert-boxes-styled-blockquotes) | object | | `ordered_list_point_1`
      `ordered_list_point_2`
      `ordered_list_point_x` | Markdown ordered list point level
      See [Markdown ordered list point](#markdown-ordered-list-point) | object | | `ordered_list_1`
      `ordered_list_2`
      `ordered_list_x` | Markdown ordered list level
      See [Markdown ordered list](#markdown-ordered-list) | object | | `unordered_list_point_1`
      `unordered_list_point_2`
      `unordered_list_point_x` | Markdown unordered list point level
      See [Markdown unordered list point](#markdown-unordered-list-point) | object | @@ -621,14 +650,14 @@ overview: table: {} ``` -| Key | Description | Data type | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------|-----------| -| `group_heading` | **Overview group heading**
      Styling for the group label if grouping is activated
      See [Overview group heading](#overview-group-heading) | object | -| `table` | **Overview table**
      See [Overview table](#overview-table) | object | +| Key | Description | Data type | +| - | - | - | +| `group_heading` | **Overview group heading**
      Styling for the group lavel if grouping is activated
      See [Overview group heading](#overview-group-heading) | object | +| `table` | **Overview table**
      See [Overview table](#overview-table) | object | ## Overview group heading -Styling for the group label if grouping is activated +Styling for the group lavel if grouping is activated Key: `group_heading` @@ -1092,10 +1121,10 @@ subject: margin_bottom: 10 ``` -| Key | Description | Data type | -|-----|---------------------------------------------|-----------| -| … | See [Font properties](#font-properties) | | -| … | See [Margin properties](#margin-properties) | | +| Key | Description | Data type | +| - | - | - | +| … | See [Font properties](#font-properties) | | +| … | See [Margin properties](#margin-properties) | | ## Work package subject level @@ -1123,6 +1152,18 @@ subject_level_3: | … | See [Font properties](#font-properties) | | | … | See [Margin properties](#margin-properties) | | +## alert boxes (styled blockquotes) + +Key: `alerts` + +| Key | Description | Data type | +| - | - | - | +| `NOTE` | **Alert**
      Styling to denote a quote as alert box
      See [Alert](#alert) | object | +| `TIP` | **Alert**
      Styling to denote a quote as alert box
      See [Alert](#alert) | object | +| `WARNING` | **Alert**
      Styling to denote a quote as alert box
      See [Alert](#alert) | object | +| `IMPORTANT` | **Alert**
      Styling to denote a quote as alert box
      See [Alert](#alert) | object | +| `CAUTION` | **Alert**
      Styling to denote a quote as alert box
      See [Alert](#alert) | object | + ## Units available units are diff --git a/docs/user-guide/file-management/nextcloud-integration/README.md b/docs/user-guide/file-management/nextcloud-integration/README.md index 4b970b7427cc..fd9947000c87 100644 --- a/docs/user-guide/file-management/nextcloud-integration/README.md +++ b/docs/user-guide/file-management/nextcloud-integration/README.md @@ -39,6 +39,10 @@ It is also possible to automatically create dedicated [project folders](../../pr | [Permissions and access control](#permissions-and-access-control) | Who has access to linked files and who doesn't | | [Possible errors and troubleshooting](#possible-errors-and-troubleshooting) | Common errors and how to troubleshoot them | +This video will give you a complete overview of how to set-up and work with the Nextcloud integration (English only): + +![Nextcloud integration complete user guide and admin guide](https://openproject-docs.s3.eu-central-1.amazonaws.com/videos/OpenProject-NextCloud-integration.mp4) + ## Connect your OpenProject and Nextcloud accounts To begin using this integration, you will need to first connect your OpenProject and Nextcloud accounts. To do this, open any work package in a project where a Nextcloud file storage has been added and enabled by an administrator and follow these steps: diff --git a/docs/user-guide/projects/project-lists/README.md b/docs/user-guide/projects/project-lists/README.md index f0c2d9c961ca..860f4ff7905d 100644 --- a/docs/user-guide/projects/project-lists/README.md +++ b/docs/user-guide/projects/project-lists/README.md @@ -120,7 +120,7 @@ Your saved project lists filter will appear on the left side under **My private You can always rename or remove your private project lists by using the respective option. -> **Note:** static lists and lists, that have been shared with other users cannot be renamed, so the option will not be displayed. +> **Note:** Static lists cannot be renamed, so the option will not be displayed here. ![Delete a private projects filter in OpenProject](private-project-filter-rename-delete.png) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a41a27227e69..28644f52e6ca 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -47,9 +47,9 @@ "@ngneat/content-loader": "^7.0.0", "@ngx-formly/core": "^6.1.4", "@openproject/octicons-angular": "^19.14.1", - "@openproject/primer-view-components": "^0.34.0", + "@openproject/primer-view-components": "^0.35.2", "@openproject/reactivestates": "^3.0.1", - "@primer/css": "^21.2.2", + "@primer/css": "^21.3.3", "@types/hotwired__turbo": "^8.0.1", "@uirouter/angular": "^13.0.0", "@uirouter/core": "^6.1.0", @@ -3337,18 +3337,18 @@ } }, "node_modules/@floating-ui/dom": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", - "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.6.tgz", + "integrity": "sha512-qiTYajAnh3P+38kECeffMSQgbvXty2VB6rS+42iWR4FPIlZjLK84E9qtLnMTLIpPz2znD/TaFqaiavMUrS+Hcw==", "dependencies": { "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" + "@floating-ui/utils": "^0.2.3" } }, "node_modules/@floating-ui/utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.3.tgz", + "integrity": "sha512-XGndio0l5/Gvd6CLIABvsav9HHezgDFFhDfHk1bvLfr9ni8dojqLSvBbotJEjmIwNHL7vK4QzBJTdBRoB+c1ww==" }, "node_modules/@fullcalendar/angular": { "version": "6.1.14", @@ -4036,10 +4036,9 @@ } }, "node_modules/@ng-select/ng-select": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-13.2.0.tgz", - "integrity": "sha512-Oh4UaUvYJa6D+G4nEcdzqOgRP2TKqwwRpQBVVyLMUgzebSY3Cw9aJXqudqJ/EYiK3Bz+4uKJXPZIrYgNqqx02g==", - "license": "MIT", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-13.3.0.tgz", + "integrity": "sha512-MT5q5PemshfVP4OpjdLJY27G0ZRVhi8WHn0ZSVrRGk1vztmi5VIXSefGoCpsZkay1IM4Wzlm8PmHbgWqnJKqCA==", "dependencies": { "tslib": "^2.3.1" }, @@ -4080,9 +4079,9 @@ } }, "node_modules/@ngx-formly/core": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.3.4.tgz", - "integrity": "sha512-xpO0FB50gpQI4uDrz6nYNolMvMIOdgP19diflcvAAtDy15I4BS+tzLKOCkM9HmAwmKQ59z3FUyzaODFEu+cuEg==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.3.5.tgz", + "integrity": "sha512-9p4yl7fr2Ojmm/uN7/nM1hYezheUxecEC0WZ0YI6jeSoEJR8NYTglVxTmHrpW5had2oolHeO39sAo9ttJNifSA==", "dependencies": { "tslib": "^2.0.0" }, @@ -4817,10 +4816,9 @@ } }, "node_modules/@openproject/primer-view-components": { - "version": "0.34.0", - "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.34.0.tgz", - "integrity": "sha512-OwPhQTEjQFN3vgXeAuxtlAFYOFRgCltKJv47qDtJ1l90xOhL2/bb7ny2SjtvYVwCo6jzdBv46WKDBSc7Zq2OBg==", - "license": "MIT", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.35.2.tgz", + "integrity": "sha512-SiUJeelthWTWuW96Xz1ZRC71Sxbs7zI0BKw/MKTkc46pX0z4cItelt4qoLWSGIl1ZLrjNe+dFNXqoVJLIjxnYw==", "dependencies": { "@github/auto-check-element": "^5.2.0", "@github/auto-complete-element": "^3.6.2", @@ -4867,12 +4865,12 @@ "integrity": "sha512-HWwz+6MrfK5NTWcg9GdKFpMBW/yrAV937oXiw2eDtsd88P3SRwoCt6ZO6QmKp9RP3nDU9cbqmuGZ0xBh0eIFeg==" }, "node_modules/@primer/css": { - "version": "21.3.3", - "resolved": "https://registry.npmjs.org/@primer/css/-/css-21.3.3.tgz", - "integrity": "sha512-mxmDPVRPfYdh9UgR13/Fl6ndMb+0aunZ1crApQEMxdOHZugHcuqxS4eSK+LLb8MJQ4Z3yduJdiXyqMfX9jHuPA==", + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/@primer/css/-/css-21.3.4.tgz", + "integrity": "sha512-MjeMlwnnttbWFSfBiG+j2VKSaQcoCIoTcbYpjAPWChKV4SkY1xxW6/LeJTE8cyqcL7ujdE8IF0B7m0W+BpCN2w==", "dependencies": { "@primer/primitives": "^8.2.0", - "@primer/view-components": "^0.25.1" + "@primer/view-components": "^0.26.1" }, "engines": { "node": ">=16.0.0" @@ -4885,9 +4883,9 @@ }, "node_modules/@primer/view-components": { "name": "@openproject/primer-view-components", - "version": "0.33.1", - "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.33.1.tgz", - "integrity": "sha512-lOKqObPWYfuscBJnj0fFuyq8Lz/sQYBlXsvFAs3N1Nu0XSDH/Ak97bFcuIIX4lRWxnyGWSj/BREvIjCoyi2BCA==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.35.2.tgz", + "integrity": "sha512-SiUJeelthWTWuW96Xz1ZRC71Sxbs7zI0BKw/MKTkc46pX0z4cItelt4qoLWSGIl1ZLrjNe+dFNXqoVJLIjxnYw==", "dependencies": { "@github/auto-check-element": "^5.2.0", "@github/auto-complete-element": "^3.6.2", @@ -5551,9 +5549,9 @@ } }, "node_modules/@types/jqueryui": { - "version": "1.12.22", - "resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.22.tgz", - "integrity": "sha512-4r7ROoUJ5gaIWvQa2qAHyrhskJcUNM62Md8M9+4DtabEiIQ9Y0pVlW88ojyXvn4M1HNUc/47KpFJaXhrk8P/rg==", + "version": "1.12.23", + "resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.23.tgz", + "integrity": "sha512-pm1yVNVI29B9IGw41anCEzA5eR2r1pYc7flqD471ZT7B0yUXIY7YNe/zq7LGpihIGXNzWyG+Q4YQSzv2AF3fNA==", "dev": true, "dependencies": { "@types/jquery": "*" @@ -5571,9 +5569,9 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", - "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==", + "version": "4.17.6", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz", + "integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==", "dev": true }, "node_modules/@types/mime": { @@ -5713,16 +5711,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.1.tgz", - "integrity": "sha512-kZqi+WZQaZfPKnsflLJQCz6Ze9FFSMfXrrIOcyargekQxG37ES7DJNpJUE9Q/X5n3yTIP/WPutVNzgknQ7biLg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz", + "integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/type-utils": "7.13.1", - "@typescript-eslint/utils": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/type-utils": "7.14.1", + "@typescript-eslint/utils": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -5746,13 +5744,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.1.tgz", - "integrity": "sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz", + "integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1" + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -5763,13 +5761,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.13.1.tgz", - "integrity": "sha512-aWDbLu1s9bmgPGXSzNCxELu+0+HQOapV/y+60gPXafR8e2g1Bifxzevaa+4L2ytCWm+CHqpELq4CSoN9ELiwCg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz", + "integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.13.1", - "@typescript-eslint/utils": "7.13.1", + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/utils": "7.14.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -5790,9 +5788,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.1.tgz", - "integrity": "sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz", + "integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -5803,13 +5801,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.1.tgz", - "integrity": "sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz", + "integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -5831,15 +5829,15 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.1.tgz", - "integrity": "sha512-h5MzFBD5a/Gh/fvNdp9pTfqJAbuQC4sCN2WzuXme71lqFJsZtLbjxfSk4r3p02WIArOF9N94pdsLiGutpDbrXQ==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz", + "integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/typescript-estree": "7.13.1" + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -5853,12 +5851,12 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.1.tgz", - "integrity": "sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz", + "integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/types": "7.14.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -5879,9 +5877,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -5894,15 +5892,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.13.1.tgz", - "integrity": "sha512-1ELDPlnLvDQ5ybTSrMhRTFDfOQEOXNM+eP+3HT/Yq7ruWpciQw+Avi73pdEbA4SooCawEWo3dtYbF68gN7Ed1A==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/typescript-estree": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "debug": "^4.3.4" }, "engines": { @@ -5922,13 +5920,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.1.tgz", - "integrity": "sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz", + "integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1" + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -5939,9 +5937,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.1.tgz", - "integrity": "sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz", + "integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -5952,13 +5950,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.1.tgz", - "integrity": "sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz", + "integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -5980,12 +5978,12 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.1.tgz", - "integrity": "sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz", + "integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/types": "7.14.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -24281,18 +24279,18 @@ } }, "@floating-ui/dom": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", - "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.6.tgz", + "integrity": "sha512-qiTYajAnh3P+38kECeffMSQgbvXty2VB6rS+42iWR4FPIlZjLK84E9qtLnMTLIpPz2znD/TaFqaiavMUrS+Hcw==", "requires": { "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" + "@floating-ui/utils": "^0.2.3" } }, "@floating-ui/utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.3.tgz", + "integrity": "sha512-XGndio0l5/Gvd6CLIABvsav9HHezgDFFhDfHk1bvLfr9ni8dojqLSvBbotJEjmIwNHL7vK4QzBJTdBRoB+c1ww==" }, "@fullcalendar/angular": { "version": "6.1.14", @@ -24851,9 +24849,9 @@ } }, "@ng-select/ng-select": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-13.2.0.tgz", - "integrity": "sha512-Oh4UaUvYJa6D+G4nEcdzqOgRP2TKqwwRpQBVVyLMUgzebSY3Cw9aJXqudqJ/EYiK3Bz+4uKJXPZIrYgNqqx02g==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-13.3.0.tgz", + "integrity": "sha512-MT5q5PemshfVP4OpjdLJY27G0ZRVhi8WHn0ZSVrRGk1vztmi5VIXSefGoCpsZkay1IM4Wzlm8PmHbgWqnJKqCA==", "requires": { "tslib": "^2.3.1" } @@ -24872,9 +24870,9 @@ "integrity": "sha512-CjSVVa/9fzMpEDQP01SC4colKCbZwj7vUq0H2bivp8jVsmd21x9Fu0gDBH0Y9NdfAIm4eGZvmiZKMII3vIOaYQ==" }, "@ngx-formly/core": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.3.4.tgz", - "integrity": "sha512-xpO0FB50gpQI4uDrz6nYNolMvMIOdgP19diflcvAAtDy15I4BS+tzLKOCkM9HmAwmKQ59z3FUyzaODFEu+cuEg==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.3.5.tgz", + "integrity": "sha512-9p4yl7fr2Ojmm/uN7/nM1hYezheUxecEC0WZ0YI6jeSoEJR8NYTglVxTmHrpW5had2oolHeO39sAo9ttJNifSA==", "requires": { "tslib": "^2.0.0" } @@ -25360,9 +25358,9 @@ } }, "@openproject/primer-view-components": { - "version": "0.34.0", - "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.34.0.tgz", - "integrity": "sha512-OwPhQTEjQFN3vgXeAuxtlAFYOFRgCltKJv47qDtJ1l90xOhL2/bb7ny2SjtvYVwCo6jzdBv46WKDBSc7Zq2OBg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.35.2.tgz", + "integrity": "sha512-SiUJeelthWTWuW96Xz1ZRC71Sxbs7zI0BKw/MKTkc46pX0z4cItelt4qoLWSGIl1ZLrjNe+dFNXqoVJLIjxnYw==", "requires": { "@github/auto-check-element": "^5.2.0", "@github/auto-complete-element": "^3.6.2", @@ -25403,12 +25401,12 @@ "integrity": "sha512-HWwz+6MrfK5NTWcg9GdKFpMBW/yrAV937oXiw2eDtsd88P3SRwoCt6ZO6QmKp9RP3nDU9cbqmuGZ0xBh0eIFeg==" }, "@primer/css": { - "version": "21.3.3", - "resolved": "https://registry.npmjs.org/@primer/css/-/css-21.3.3.tgz", - "integrity": "sha512-mxmDPVRPfYdh9UgR13/Fl6ndMb+0aunZ1crApQEMxdOHZugHcuqxS4eSK+LLb8MJQ4Z3yduJdiXyqMfX9jHuPA==", + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/@primer/css/-/css-21.3.4.tgz", + "integrity": "sha512-MjeMlwnnttbWFSfBiG+j2VKSaQcoCIoTcbYpjAPWChKV4SkY1xxW6/LeJTE8cyqcL7ujdE8IF0B7m0W+BpCN2w==", "requires": { "@primer/primitives": "^8.2.0", - "@primer/view-components": "npm:@openproject/primer-view-components@^0.33.1" + "@primer/view-components": "npm:@openproject/primer-view-components@^0.35.2" } }, "@primer/primitives": { @@ -25417,9 +25415,9 @@ "integrity": "sha512-K8A/DA6xv8P/kD/9DupFn+KYlo06OpcrwfwJf+sKp+KnX7ZRwLLDg1AaEGAoRoaykXRY/gfrXlgDfK7laOTWyA==" }, "@primer/view-components": { - "version": "npm:@openproject/primer-view-components@0.33.1", - "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.33.1.tgz", - "integrity": "sha512-lOKqObPWYfuscBJnj0fFuyq8Lz/sQYBlXsvFAs3N1Nu0XSDH/Ak97bFcuIIX4lRWxnyGWSj/BREvIjCoyi2BCA==", + "version": "npm:@openproject/primer-view-components@0.35.2", + "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.35.2.tgz", + "integrity": "sha512-SiUJeelthWTWuW96Xz1ZRC71Sxbs7zI0BKw/MKTkc46pX0z4cItelt4qoLWSGIl1ZLrjNe+dFNXqoVJLIjxnYw==", "requires": { "@github/auto-check-element": "^5.2.0", "@github/auto-complete-element": "^3.6.2", @@ -25926,9 +25924,9 @@ } }, "@types/jqueryui": { - "version": "1.12.22", - "resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.22.tgz", - "integrity": "sha512-4r7ROoUJ5gaIWvQa2qAHyrhskJcUNM62Md8M9+4DtabEiIQ9Y0pVlW88ojyXvn4M1HNUc/47KpFJaXhrk8P/rg==", + "version": "1.12.23", + "resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.23.tgz", + "integrity": "sha512-pm1yVNVI29B9IGw41anCEzA5eR2r1pYc7flqD471ZT7B0yUXIY7YNe/zq7LGpihIGXNzWyG+Q4YQSzv2AF3fNA==", "dev": true, "requires": { "@types/jquery": "*" @@ -25946,9 +25944,9 @@ "dev": true }, "@types/lodash": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", - "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==", + "version": "4.17.6", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz", + "integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==", "dev": true }, "@types/mime": { @@ -26090,16 +26088,16 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.1.tgz", - "integrity": "sha512-kZqi+WZQaZfPKnsflLJQCz6Ze9FFSMfXrrIOcyargekQxG37ES7DJNpJUE9Q/X5n3yTIP/WPutVNzgknQ7biLg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz", + "integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/type-utils": "7.13.1", - "@typescript-eslint/utils": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/type-utils": "7.14.1", + "@typescript-eslint/utils": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -26107,41 +26105,41 @@ }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.1.tgz", - "integrity": "sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz", + "integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1" + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1" } }, "@typescript-eslint/type-utils": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.13.1.tgz", - "integrity": "sha512-aWDbLu1s9bmgPGXSzNCxELu+0+HQOapV/y+60gPXafR8e2g1Bifxzevaa+4L2ytCWm+CHqpELq4CSoN9ELiwCg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz", + "integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "7.13.1", - "@typescript-eslint/utils": "7.13.1", + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/utils": "7.14.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/types": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.1.tgz", - "integrity": "sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz", + "integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.1.tgz", - "integrity": "sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz", + "integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -26151,24 +26149,24 @@ } }, "@typescript-eslint/utils": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.1.tgz", - "integrity": "sha512-h5MzFBD5a/Gh/fvNdp9pTfqJAbuQC4sCN2WzuXme71lqFJsZtLbjxfSk4r3p02WIArOF9N94pdsLiGutpDbrXQ==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz", + "integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/typescript-estree": "7.13.1" + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1" } }, "@typescript-eslint/visitor-keys": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.1.tgz", - "integrity": "sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz", + "integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/types": "7.14.1", "eslint-visitor-keys": "^3.4.3" } }, @@ -26182,9 +26180,9 @@ } }, "minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -26193,42 +26191,42 @@ } }, "@typescript-eslint/parser": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.13.1.tgz", - "integrity": "sha512-1ELDPlnLvDQ5ybTSrMhRTFDfOQEOXNM+eP+3HT/Yq7ruWpciQw+Avi73pdEbA4SooCawEWo3dtYbF68gN7Ed1A==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/typescript-estree": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "debug": "^4.3.4" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.1.tgz", - "integrity": "sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz", + "integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1" + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1" } }, "@typescript-eslint/types": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.1.tgz", - "integrity": "sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz", + "integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.1.tgz", - "integrity": "sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz", + "integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -26238,12 +26236,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.1.tgz", - "integrity": "sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz", + "integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/types": "7.14.1", "eslint-visitor-keys": "^3.4.3" } }, diff --git a/frontend/package.json b/frontend/package.json index 32bc896ac509..79e6263d4f9c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -98,9 +98,9 @@ "@ngneat/content-loader": "^7.0.0", "@ngx-formly/core": "^6.1.4", "@openproject/octicons-angular": "^19.14.1", - "@openproject/primer-view-components": "^0.34.0", + "@openproject/primer-view-components": "^0.35.2", "@openproject/reactivestates": "^3.0.1", - "@primer/css": "^21.2.2", + "@primer/css": "^21.3.3", "@types/hotwired__turbo": "^8.0.1", "@uirouter/angular": "^13.0.0", "@uirouter/core": "^6.1.0", @@ -175,6 +175,6 @@ "generate-typings": "tsc -d -p src/tsconfig.app.json" }, "overrides": { - "@primer/view-components": "npm:@openproject/primer-view-components@^0.33.1" + "@primer/view-components": "npm:@openproject/primer-view-components@^0.35.2" } } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 85a1357c1731..1b037c1dba91 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -164,6 +164,7 @@ import { ReminderSettingsPageComponent, } from 'core-app/features/user-preferences/reminder-settings/page/reminder-settings-page.component'; import { OpenProjectMyAccountModule } from 'core-app/features/user-preferences/user-preferences.module'; +import { OpAttachmentsComponent } from 'core-app/shared/components/attachments/attachments.component'; export function initializeServices(injector:Injector) { return () => { @@ -352,6 +353,9 @@ export class OpenProjectModule { registerCustomElement('opce-draggable-autocompleter', DraggableAutocompleteComponent, { injector }); registerCustomElement('opce-attribute-help-text', AttributeHelpTextComponent, { injector }); registerCustomElement('opce-exclusion-info', OpExclusionInfoComponent, { injector }); + registerCustomElement('opce-attachments', OpAttachmentsComponent, { injector }); + + // TODO: These elements are now registered custom elements, but are actually single-use components. They should be removed when we move these pages to Rails. registerCustomElement('opce-new-project', NewProjectComponent, { injector }); registerCustomElement('opce-project-settings', ProjectsComponent, { injector }); registerCustomElement('opce-copy-project', CopyProjectComponent, { injector }); diff --git a/frontend/src/app/core/main-menu/submenu.service.ts b/frontend/src/app/core/main-menu/submenu.service.ts new file mode 100644 index 000000000000..95a9cf3cc2e8 --- /dev/null +++ b/frontend/src/app/core/main-menu/submenu.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { TurboElement } from 'core-typings/turbo'; +import { StateService } from '@uirouter/core'; + +@Injectable({ providedIn: 'root' }) +export class SubmenuService { + constructor(protected $state:StateService) {} + + reloadSubmenu(selectedQueryId:string|null):void { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment + const menuIdentifier:string|undefined = this.$state.current.data.sideMenuOptions?.sidemenuId; + + if (menuIdentifier) { + const menu = (document.getElementById(menuIdentifier) as HTMLElement&TurboElement); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const sideMenuOptions = this.$state.$current.data?.sideMenuOptions as { hardReloadOnBaseRoute?:boolean, defaultQuery?:string }; + const currentSrc = menu.getAttribute('src'); + + if (currentSrc && menu) { + const frameUrl = new URL(currentSrc); + const defaultQuery = sideMenuOptions.defaultQuery; + + if (selectedQueryId) { + // If there is a default query passed in the route definition, it means that id passed as argument and not as parameter, + // e.g. calendars/:id, team_planner/:id, ... + // Otherwise, we will just replace the params + if (defaultQuery) { + frameUrl.search = `?id=${selectedQueryId}`; + } else { + frameUrl.search = `?query_id=${selectedQueryId}`; + } + } + + // Override the frame src to enforce a reload + menu.setAttribute('src', frameUrl.href); + } + } + } +} diff --git a/frontend/src/app/core/setup/global-dynamic-components.const.ts b/frontend/src/app/core/setup/global-dynamic-components.const.ts index a4ad79800833..a309bca94dee 100644 --- a/frontend/src/app/core/setup/global-dynamic-components.const.ts +++ b/frontend/src/app/core/setup/global-dynamic-components.const.ts @@ -8,10 +8,6 @@ import { ZenModeButtonComponent, zenModeComponentSelector, } from 'core-app/features/work-packages/components/wp-buttons/zen-mode-toggle-button/zen-mode-toggle-button.component'; -import { - attachmentsSelector, - OpAttachmentsComponent, -} from 'core-app/shared/components/attachments/attachments.component'; import { GlobalSearchWorkPackagesComponent, globalSearchWorkPackagesSelector, @@ -20,7 +16,6 @@ import { CustomDateActionAdminComponent, customDateActionAdminSelector, } from 'core-app/features/work-packages/components/wp-custom-actions/date-action/custom-date-action-admin.component'; -import { BoardsMenuComponent, boardsMenuSelector } from 'core-app/features/boards/boards-sidebar/boards-menu.component'; import { GlobalSearchWorkPackagesEntryComponent, globalSearchWorkPackagesSelectorEntry, @@ -143,10 +138,6 @@ import { opInAppNotificationBellSelector, } from 'core-app/features/in-app-notifications/bell/in-app-notification-bell.component'; import { IanMenuComponent, ianMenuSelector } from 'core-app/features/in-app-notifications/center/menu/menu.component'; -import { - opTeamPlannerSidemenuSelector, - TeamPlannerSidemenuComponent, -} from 'core-app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component'; import { OpModalOverlayComponent, opModalOverlaySelector, @@ -182,12 +173,10 @@ export const globalDynamicComponents:OptionalBootstrapDefinition[] = [ { selector: staticAttributeHelpTextSelector, cls: StaticAttributeHelpTextComponent }, { selector: colorsAutocompleterSelector, cls: ColorsAutocompleterComponent }, { selector: zenModeComponentSelector, cls: ZenModeButtonComponent }, - { selector: attachmentsSelector, cls: OpAttachmentsComponent, embeddable: true }, { selector: globalSearchTabsSelector, cls: GlobalSearchTabsComponent }, { selector: globalSearchWorkPackagesSelector, cls: GlobalSearchWorkPackagesComponent }, { selector: homescreenNewFeaturesBlockSelector, cls: HomescreenNewFeaturesBlockComponent }, { selector: customDateActionAdminSelector, cls: CustomDateActionAdminComponent }, - { selector: boardsMenuSelector, cls: BoardsMenuComponent }, { selector: globalSearchWorkPackagesSelectorEntry, cls: GlobalSearchWorkPackagesEntryComponent }, { selector: toastsContainerSelector, cls: ToastsContainerComponent }, { selector: sidemenuSelector, cls: OpSidemenuComponent }, @@ -213,7 +202,6 @@ export const globalDynamicComponents:OptionalBootstrapDefinition[] = [ { selector: headerProjectSelectSelector, cls: OpHeaderProjectSelectComponent }, { selector: wpOverviewGraphSelector, cls: WorkPackageOverviewGraphComponent }, { selector: opViewSelectSelector, cls: ViewSelectComponent }, - { selector: opTeamPlannerSidemenuSelector, cls: TeamPlannerSidemenuComponent }, { selector: triggerActionsEntryComponentSelector, cls: TriggerActionsEntryComponent, embeddable: true }, { selector: editableQueryPropsSelector, cls: EditableQueryPropsComponent }, { selector: backupSelector, cls: BackupComponent }, diff --git a/frontend/src/app/core/setup/globals/onboarding/helpers.ts b/frontend/src/app/core/setup/globals/onboarding/helpers.ts index 7eea4eb9d5b9..ee162ca39017 100644 --- a/frontend/src/app/core/setup/globals/onboarding/helpers.ts +++ b/frontend/src/app/core/setup/globals/onboarding/helpers.ts @@ -1,9 +1,5 @@ export const onboardingTourStorageKey = 'openProject-onboardingTour'; -export type OnboardingTourNames = 'prepareBacklogs'|'backlogs'|'taskboard'|'homescreen'|'workPackages'|'main'; - -export enum ProjectName { - demo = 'demo', -} +export type OnboardingTourNames = 'homescreen'|'workPackages'|'gantt'|'final'|'boards'|'teamPlanner'; function matchingFilter(list:NodeListOf, filterFunction:(match:HTMLElement) => boolean):HTMLElement|null { for (let i = 0; i < list.length; i++) { diff --git a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts index 0862cd8208c0..575f0ce59510 100644 --- a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts +++ b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts @@ -2,13 +2,18 @@ import { wpOnboardingTourSteps } from 'core-app/core/setup/globals/onboarding/to import { OnboardingTourNames, onboardingTourStorageKey, - ProjectName, waitForElement, } from 'core-app/core/setup/globals/onboarding/helpers'; -import { boardTourSteps } from 'core-app/core/setup/globals/onboarding/tours/boards_tour'; +import { + boardTourSteps, + navigateToBoardStep, +} from 'core-app/core/setup/globals/onboarding/tours/boards_tour'; import { menuTourSteps } from 'core-app/core/setup/globals/onboarding/tours/menu_tour'; import { homescreenOnboardingTourSteps } from 'core-app/core/setup/globals/onboarding/tours/homescreen_tour'; -import { teamPlannerTourSteps } from 'core-app/core/setup/globals/onboarding/tours/team_planners_tour'; +import { + navigateToTeamPlannerStep, + teamPlannerTourSteps, +} from 'core-app/core/setup/globals/onboarding/tours/team_planners_tour'; import { ganttOnboardingTourSteps } from 'core-app/core/setup/globals/onboarding/tours/gantt_tour'; require('core-vendor/enjoyhint'); @@ -36,9 +41,11 @@ export type OnboardingStep = { }; function initializeTour(storageValue:string) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment window.onboardingTourInstance = new window.EnjoyHint({ onStart() { jQuery('#content-wrapper, #menu-sidebar').addClass('-hidden-overflow'); + sessionStorage.setItem(onboardingTourStorageKey, storageValue); }, onEnd() { sessionStorage.setItem(onboardingTourStorageKey, storageValue); @@ -56,8 +63,8 @@ function startTour(steps:OnboardingStep[]) { window.onboardingTourInstance.run(); } -function moduleVisible(name:string):boolean { - return document.getElementsByClassName(`${name}-menu-item`).length > 0; +export function moduleVisible(name:string):boolean { + return document.querySelector(`#menu-sidebar .${name}-menu-item`) !== null; } function workPackageTour() { @@ -68,8 +75,9 @@ function workPackageTour() { startTour(steps); }); } -function mainTour(project:ProjectName = ProjectName.demo) { - initializeTour('mainTourFinished'); + +function ganttTour() { + initializeTour('ganttTourFinished'); const boardsDemoDataAvailable = jQuery('meta[name=boards_demo_data_available]').attr('content') === 'true'; const teamPlannerDemoDataAvailable = jQuery('meta[name=demo_view_of_type_team_planner_seeded]').attr('content') === 'true'; @@ -77,30 +85,59 @@ function mainTour(project:ProjectName = ProjectName.demo) { waitForElement('.work-package--results-tbody', '#content', () => { let steps:OnboardingStep[] = ganttOnboardingTourSteps(); - // Check for EE edition if (eeTokenAvailable) { // ... and available seed data of boards. // Then add boards to the tour, otherwise skip it. if (boardsDemoDataAvailable && moduleVisible('boards')) { - steps = steps.concat(boardTourSteps('enterprise', project)); - } - - // ... same for team planners - if (teamPlannerDemoDataAvailable && moduleVisible('team-planner-view')) { - steps = steps.concat(teamPlannerTourSteps()); + steps = steps.concat(navigateToBoardStep('enterprise')); + } else if (teamPlannerDemoDataAvailable && moduleVisible('team-planner-view')) { + steps = steps.concat(navigateToTeamPlannerStep()); + } else { + steps = steps.concat(menuTourSteps()); } } else if (boardsDemoDataAvailable && moduleVisible('boards')) { - steps = steps.concat(boardTourSteps('basic', project)); + steps = steps.concat(navigateToBoardStep('basic')); + } else { + steps = steps.concat(menuTourSteps()); + } + + startTour(steps); + }); +} + +function boardTour() { + initializeTour('boardsTourFinished'); + + const teamPlannerDemoDataAvailable = jQuery('meta[name=demo_view_of_type_team_planner_seeded]').attr('content') === 'true'; + const eeTokenAvailable = !jQuery('body').hasClass('ee-banners-visible'); + + waitForElement('wp-single-card', '#content', () => { + let steps:OnboardingStep[] = eeTokenAvailable ? boardTourSteps('enterprise') : boardTourSteps('basic'); + + // Available seed data of team planner. + // Then add Team planner to the tour, otherwise skip it. + if (teamPlannerDemoDataAvailable && moduleVisible('team-planner-view')) { + steps = steps.concat(navigateToTeamPlannerStep()); + } else { + steps = steps.concat(menuTourSteps()); } + startTour(steps); + }); +} + +function teamPlannerTour() { + initializeTour('teamPlannerTourFinished'); + waitForElement('full-calendar', '#content', () => { + let steps:OnboardingStep[] = teamPlannerTourSteps(); steps = steps.concat(menuTourSteps()); startTour(steps); }); } -export function start(name:OnboardingTourNames, project?:ProjectName):void { +export function start(name:OnboardingTourNames):void { switch (name) { case 'homescreen': initializeTour('startProjectTour'); @@ -109,8 +146,14 @@ export function start(name:OnboardingTourNames, project?:ProjectName):void { case 'workPackages': workPackageTour(); break; - case 'main': - mainTour(project); + case 'gantt': + ganttTour(); + break; + case 'boards': + boardTour(); + break; + case 'teamPlanner': + teamPlannerTour(); break; default: break; diff --git a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour_trigger.ts b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour_trigger.ts index 0dd1be044a1b..c7c4be84a716 100644 --- a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour_trigger.ts +++ b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour_trigger.ts @@ -3,15 +3,14 @@ import { OnboardingTourNames, onboardingTourStorageKey, - ProjectName, waitForElement, } from 'core-app/core/setup/globals/onboarding/helpers'; import { debugLog } from 'core-app/shared/helpers/debug_output'; -async function triggerTour(name:OnboardingTourNames, project?:ProjectName):Promise { +async function triggerTour(name:OnboardingTourNames):Promise { debugLog(`Loading and triggering onboarding tour ${name}`); await import(/* webpackChunkName: "onboarding-tour" */ './onboarding_tour').then((tour) => { - tour.start(name, project); + tour.start(name); }); } @@ -67,12 +66,40 @@ export function detectOnboardingTour():void { // ------------------------------- Tutorial WP page ------------------------------- if (url.searchParams.get('start_onboarding_tour')) { - void triggerTour('workPackages', ProjectName.demo); + void triggerTour('workPackages'); } - // ------------------------------- Tutorial Main part (starting from the Gantt module) ------------------------------- + // ------------------------------- Tutorial Gantt module ------------------------------- if (currentTourPart === 'wpTourFinished') { - void triggerTour('main', ProjectName.demo); + void triggerTour('gantt'); + return; + } + + // ------------------------------- Tutorial Boards module ------------------------------- + if (currentTourPart === 'ganttTourFinished') { + if (url.pathname.includes('boards')) { + void triggerTour('boards'); + return; + } + if (url.pathname.includes('team_planner')) { + void triggerTour('teamPlanner'); + return; + } + void triggerTour('final'); + } + + // ------------------------------- Tutorial TeamPlanner module ------------------------------- + if (currentTourPart === 'boardsTourFinished') { + if (url.pathname.includes('team_planner')) { + void triggerTour('teamPlanner'); + return; + } + void triggerTour('final'); + } + + // ------------------------------- Fina tutorial ------------------------------- + if (currentTourPart === 'teamPlannerTourFinished') { + void triggerTour('final'); } } } diff --git a/frontend/src/app/core/setup/globals/onboarding/tours/boards_tour.ts b/frontend/src/app/core/setup/globals/onboarding/tours/boards_tour.ts index 69d9a39f4b6d..0e0d9e973f8c 100644 --- a/frontend/src/app/core/setup/globals/onboarding/tours/boards_tour.ts +++ b/frontend/src/app/core/setup/globals/onboarding/tours/boards_tour.ts @@ -1,34 +1,12 @@ import { - ProjectName, waitForElement, } from 'core-app/core/setup/globals/onboarding/helpers'; import { OnboardingStep } from 'core-app/core/setup/globals/onboarding/onboarding_tour'; -export function boardTourSteps(edition:'basic'|'enterprise', project:ProjectName):OnboardingStep[] { - let boardName:string; - if (edition === 'basic') { - boardName = project === ProjectName.demo ? 'Basic board' : 'Task board'; - } else { - boardName = 'Kanban'; - } - +export function boardTourSteps(edition:'basic'|'enterprise'):OnboardingStep[] { const listExplanation = edition === 'basic' ? 'basic' : 'kanban'; return [ - { - 'next #boards-wrapper>.boards-menu-item': I18n.t('js.onboarding.steps.boards.overview'), - showSkip: false, - nextButton: { text: I18n.t('js.onboarding.buttons.next') }, - onNext() { - jQuery('#boards-wrapper>.boards-menu-item ~ .toggler')[0].click(); - waitForElement( - '.op-sidemenu--item-action', - '#main-menu', - (match) => match.click(), - (match) => !!match.textContent?.includes(boardName), - ); - }, - }, { 'next [data-tour-selector="op-board-list"]': I18n.t(`js.onboarding.steps.boards.lists_${listExplanation}`), showSkip: false, @@ -57,3 +35,27 @@ export function boardTourSteps(edition:'basic'|'enterprise', project:ProjectName }, ]; } + +export function navigateToBoardStep(edition:'basic'|'enterprise'):OnboardingStep { + let boardName:string; + if (edition === 'basic') { + boardName = 'Basic board'; + } else { + boardName = 'Kanban'; + } + + return { + 'next #boards-wrapper>.boards-menu-item': I18n.t('js.onboarding.steps.boards.overview'), + showSkip: false, + nextButton: { text: I18n.t('js.onboarding.buttons.next') }, + onNext() { + jQuery('#boards-wrapper>.boards-menu-item ~ .toggler')[0].click(); + waitForElement( + '.op-sidemenu--item-action', + '#main-menu', + (match) => match.click(), + (match) => !!match.textContent?.includes(boardName), + ); + }, + }; +} diff --git a/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts b/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts index 474021b106b0..e529f78f1e00 100644 --- a/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts +++ b/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts @@ -3,21 +3,6 @@ import { OnboardingStep } from 'core-app/core/setup/globals/onboarding/onboardin export function teamPlannerTourSteps():OnboardingStep[] { return [ - { - 'next .team-planner-view-menu-item': I18n.t('js.onboarding.steps.team_planner.overview'), - showSkip: false, - nextButton: { text: I18n.t('js.onboarding.buttons.next') }, - onNext() { - jQuery('.team-planner-view-menu-item ~ .toggler')[0].click(); - - waitForElement( - '.op-sidemenu--item-action', - '#main-menu', - (match) => match.click(), - (match) => !!match.textContent?.includes('Team planner'), - ); - }, - }, { 'next [data-tour-selector="op-team-planner--calendar-pane"]': I18n.t('js.onboarding.steps.team_planner.calendar'), showSkip: false, @@ -53,3 +38,21 @@ export function teamPlannerTourSteps():OnboardingStep[] { }, ]; } + +export function navigateToTeamPlannerStep():OnboardingStep { + return { + 'next .team-planner-view-menu-item': I18n.t('js.onboarding.steps.team_planner.overview'), + showSkip: false, + nextButton: { text: I18n.t('js.onboarding.buttons.next') }, + onNext() { + jQuery('.team-planner-view-menu-item ~ .toggler')[0].click(); + + waitForElement( + '.op-sidemenu--item-action', + '#main-menu', + (match) => match.click(), + (match) => !!match.textContent?.includes('Team planner'), + ); + }, + }; +} diff --git a/frontend/src/app/core/setup/globals/onboarding/tours/work_package_tour.ts b/frontend/src/app/core/setup/globals/onboarding/tours/work_package_tour.ts index 9480d1d41ee7..c92c1aeb7cd1 100644 --- a/frontend/src/app/core/setup/globals/onboarding/tours/work_package_tour.ts +++ b/frontend/src/app/core/setup/globals/onboarding/tours/work_package_tour.ts @@ -50,5 +50,11 @@ export function wpOnboardingTourSteps():OnboardingStep[] { jQuery('#main-menu-gantt')[0].click(); }, }, + { + containerClass: '-dark -hidden-arrow', + onBeforeStart() { + window.location.href = `${window.location.origin}/projects/demo-project/gantt`; + }, + }, ]; } diff --git a/frontend/src/app/features/bim/ifc_models/openproject-ifc-models.routes.ts b/frontend/src/app/features/bim/ifc_models/openproject-ifc-models.routes.ts index 1886303dbdb6..595099fbd751 100644 --- a/frontend/src/app/features/bim/ifc_models/openproject-ifc-models.routes.ts +++ b/frontend/src/app/features/bim/ifc_models/openproject-ifc-models.routes.ts @@ -35,11 +35,18 @@ import { WorkPackagesBaseComponent } from 'core-app/features/work-packages/routi import { BcfSplitLeftComponent } from 'core-app/features/bim/ifc_models/bcf/split/left/bcf-split-left.component'; import { BcfSplitRightComponent } from 'core-app/features/bim/ifc_models/bcf/split/right/bcf-split-right.component'; +export const sidemenuId = 'bim_sidemenu'; + +export const sideMenuOptions = { + sidemenuId, + hardReloadOnBaseRoute: true, +}; + export const IFC_ROUTES:Ng2StateDeclaration[] = [ { name: 'bim', parent: 'optional_project', - url: '/bcf?query_id&query_props&models&viewpoint', + url: '/bcf?query_id&query_props&models&viewpoint&name', abstract: true, component: WorkPackagesBaseComponent, redirectTo: 'bim.partitioned.list', @@ -49,6 +56,7 @@ export const IFC_ROUTES:Ng2StateDeclaration[] = [ query_props: { type: 'opQueryString', dynamic: true }, models: { type: 'opQueryString', dynamic: true }, viewpoint: { type: 'int', dynamic: true }, + name: { type: 'string', dynamic: true }, }, }, { @@ -58,6 +66,7 @@ export const IFC_ROUTES:Ng2StateDeclaration[] = [ component: IFCViewerPageComponent, data: { bodyClasses: 'router--bim', + sideMenuOptions, }, }, { @@ -67,6 +76,7 @@ export const IFC_ROUTES:Ng2StateDeclaration[] = [ baseRoute: 'bim.partitioned.list', newRoute: 'bim.partitioned.list.new', partition: '-split', + sideMenuOptions, }, reloadOnSearch: false, views: { @@ -83,6 +93,7 @@ export const IFC_ROUTES:Ng2StateDeclaration[] = [ allowMovingInEditMode: true, partition: '-left-only', successState: 'bim.partitioned.show', + sideMenuOptions, }, views: { 'content-left': { component: WorkPackageNewFullViewComponent } }, }, @@ -92,6 +103,7 @@ export const IFC_ROUTES:Ng2StateDeclaration[] = [ data: { baseRoute: 'bim.partitioned.list', partition: '-left-only', + sideMenuOptions, }, reloadOnSearch: false, redirectTo: 'bim.partitioned.show.details', diff --git a/frontend/src/app/features/boards/board/board-list/board-list-menu.component.html b/frontend/src/app/features/boards/board/board-list/board-list-menu.component.html index ac5661935924..22966a2428a6 100644 --- a/frontend/src/app/features/boards/board/board-list/board-list-menu.component.html +++ b/frontend/src/app/features/boards/board/board-list/board-list-menu.component.html @@ -1,5 +1,5 @@ + [menuItemsFactory]="menuItems"> diff --git a/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts b/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts index cc50a353861e..9aa35c277222 100644 --- a/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts +++ b/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts @@ -9,9 +9,11 @@ import { ToolbarButtonComponentDefinition, ViewPartitionState, } from 'core-app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component'; -import { StateService, TransitionService } from '@uirouter/core'; +import { + StateService, + TransitionService, +} from '@uirouter/core'; import { BoardFilterComponent } from 'core-app/features/boards/board/board-filter/board-filter.component'; -import { Board } from 'core-app/features/boards/board/board'; import { ToastService } from 'core-app/shared/components/toaster/toast.service'; import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; import { BoardService } from 'core-app/features/boards/board/board.service'; @@ -19,13 +21,10 @@ import { DragAndDropService } from 'core-app/shared/helpers/drag-and-drop/drag-a import { WorkPackageFilterButtonComponent } from 'core-app/features/work-packages/components/wp-buttons/wp-filter-button/wp-filter-button.component'; import { ZenModeButtonComponent } from 'core-app/features/work-packages/components/wp-buttons/zen-mode-toggle-button/zen-mode-toggle-button.component'; import { BoardsMenuButtonComponent } from 'core-app/features/boards/board/toolbar-menu/boards-menu-button.component'; -import { RequestSwitchmap } from 'core-app/shared/helpers/rxjs/request-switchmap'; -import { componentDestroyed } from '@w11k/ngx-componentdestroyed'; import { catchError, finalize, take, - tap, } from 'rxjs/operators'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; @@ -35,11 +34,8 @@ import { BoardFiltersService } from 'core-app/features/boards/board/board-filter import { CardViewHandlerRegistry } from 'core-app/features/work-packages/components/wp-card-view/event-handler/card-view-handler-registry'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { OpTitleService } from 'core-app/core/html/op-title.service'; -import { - EMPTY, - Observable, - of, -} from 'rxjs'; +import { EMPTY } from 'rxjs'; +import { SubmenuService } from 'core-app/core/main-menu/submenu.service'; export function boardCardViewHandlerFactory(injector:Injector) { return new CardViewHandlerRegistry(injector); @@ -152,6 +148,7 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin { readonly boardFilters:BoardFiltersService, readonly Boards:BoardService, readonly titleService:OpTitleService, + readonly submenuService:SubmenuService, ) { super(); } @@ -217,6 +214,7 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin { }), finalize(() => { this.toolbarDisabled = false; + this.reloadSidemenu(); this.cdRef.detectChanges(); }), ).subscribe(() => { @@ -244,4 +242,8 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin { protected setPartition(state:Ng2StateDeclaration) { this.currentPartition = (state.data && state.data.partition) ? state.data.partition : '-split'; } + + private reloadSidemenu():void { + this.submenuService.reloadSubmenu(null); + } } diff --git a/frontend/src/app/features/boards/boards-sidebar/boards-menu.component.html b/frontend/src/app/features/boards/boards-sidebar/boards-menu.component.html deleted file mode 100644 index 9f431caa7df3..000000000000 --- a/frontend/src/app/features/boards/boards-sidebar/boards-menu.component.html +++ /dev/null @@ -1,22 +0,0 @@ -
      - - -
      - - diff --git a/frontend/src/app/features/boards/boards-sidebar/boards-menu.component.ts b/frontend/src/app/features/boards/boards-sidebar/boards-menu.component.ts deleted file mode 100644 index d879f650b6b7..000000000000 --- a/frontend/src/app/features/boards/boards-sidebar/boards-menu.component.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { - Component, - HostBinding, - OnInit, -} from '@angular/core'; -import { Observable } from 'rxjs'; -import { BoardService } from 'core-app/features/boards/board/board.service'; -import { Board } from 'core-app/features/boards/board/board'; -import { map } from 'rxjs/operators'; -import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; -import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; -import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; -import { MainMenuNavigationService } from 'core-app/core/main-menu/main-menu-navigation.service'; -import { CurrentUserService } from 'core-app/core/current-user/current-user.service'; -import { I18nService } from 'core-app/core/i18n/i18n.service'; -import { IOpSidemenuItem } from 'core-app/shared/components/sidemenu/sidemenu.component'; -import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; - -export const boardsMenuSelector = 'boards-menu'; - -@Component({ - selector: boardsMenuSelector, - templateUrl: './boards-menu.component.html', -}) - -export class BoardsMenuComponent extends UntilDestroyedMixin implements OnInit { - @HostBinding('class.op-sidebar') className = true; - - boardOptions$:Observable = this - .apiV3Service - .boards - .observeAll() - .pipe( - map((boards:Board[]) => { - const menuItems:IOpSidemenuItem[] = boards.map((board) => ({ - title: board.name, - uiSref: 'boards.partitioned.show', - uiParams: { - board_id: board.id, - query_props: undefined, - projects: 'projects', - projectPath: this.currentProject.identifier, - }, - uiOptions: { reload: true }, - })); - - return menuItems.sort((a, b) => a.title.localeCompare(b.title)); - }), - ); - - canCreateBoards$ = this - .currentUserService - .hasCapabilities$( - 'boards/create', - this.currentProject.id || null, - ) - .pipe(this.untilDestroyed()); - - text = { - board: this.I18n.t('js.label_board'), - create_new_board: this.I18n.t('js.boards.create_new'), - }; - - constructor( - readonly boardService:BoardService, - readonly apiV3Service:ApiV3Service, - readonly currentProject:CurrentProjectService, - readonly mainMenuService:MainMenuNavigationService, - readonly currentUserService:CurrentUserService, - readonly I18n:I18nService, - readonly pathHelper:PathHelperService, - ) { - super(); - } - - ngOnInit():void { - // When activating the boards submenu, - // either initially or through click on the toggle, load the results - this.mainMenuService - .onActivate('boards') - .subscribe(() => { - this.focusBackArrow(); - void this.boardService.loadAllBoards(); - }); - } - - redirectToNewBoardForm():void { - window.location.href = this.pathHelper.newBoardsPath(this.currentProject.identifier); - } - - private focusBackArrow():void { - const buttonArrowLeft = jQuery('*[data-name="boards"] .main-menu--arrow-left-to-project'); - buttonArrowLeft.focus(); - } -} diff --git a/frontend/src/app/features/boards/openproject-boards.module.ts b/frontend/src/app/features/boards/openproject-boards.module.ts index 8151a3202c6e..e10f851a8d68 100644 --- a/frontend/src/app/features/boards/openproject-boards.module.ts +++ b/frontend/src/app/features/boards/openproject-boards.module.ts @@ -36,7 +36,6 @@ import { BoardsRootComponent } from 'core-app/features/boards/boards-root/boards import { BoardInlineAddAutocompleterComponent } from 'core-app/features/boards/board/inline-add/board-inline-add-autocompleter.component'; import { BoardsToolbarMenuDirective } from 'core-app/features/boards/board/toolbar-menu/boards-toolbar-menu.directive'; import { BoardConfigurationModalComponent } from 'core-app/features/boards/board/configuration-modal/board-configuration.modal'; -import { BoardsMenuComponent } from 'core-app/features/boards/boards-sidebar/boards-menu.component'; import { AddListModalComponent } from 'core-app/features/boards/board/add-list-modal/add-list-modal.component'; import { BoardHighlightingTabComponent } from 'core-app/features/boards/board/configuration-modal/tabs/highlighting-tab.component'; import { AddCardDropdownMenuDirective } from 'core-app/features/boards/board/add-card-dropdown/add-card-dropdown-menu.directive'; @@ -76,7 +75,6 @@ import { OpenprojectAutocompleterModule } from 'core-app/shared/components/autoc BoardListComponent, BoardsRootComponent, BoardInlineAddAutocompleterComponent, - BoardsMenuComponent, BoardHighlightingTabComponent, BoardConfigurationModalComponent, BoardsToolbarMenuDirective, diff --git a/frontend/src/app/features/boards/openproject-boards.routes.ts b/frontend/src/app/features/boards/openproject-boards.routes.ts index 70fd46aae91e..a31fad5d05bd 100644 --- a/frontend/src/app/features/boards/openproject-boards.routes.ts +++ b/frontend/src/app/features/boards/openproject-boards.routes.ts @@ -35,6 +35,12 @@ import { WorkPackageSplitViewComponent } from 'core-app/features/work-packages/r export const menuItemClass = 'boards-menu-item'; +export const sidemenuId = 'boards_sidemenu'; +export const sideMenuOptions = { + sidemenuId, + hardReloadOnBaseRoute: true, +}; + export const BOARDS_ROUTES:Ng2StateDeclaration[] = [ { name: 'boards', @@ -45,6 +51,7 @@ export const BOARDS_ROUTES:Ng2StateDeclaration[] = [ data: { bodyClasses: 'router--boards-view-base', menuItem: menuItemClass, + sideMenuOptions, }, params: { // Use custom encoder/decoder that ensures validity of URL string @@ -63,6 +70,7 @@ export const BOARDS_ROUTES:Ng2StateDeclaration[] = [ parent: 'boards', bodyClasses: 'router--boards-full-view', menuItem: menuItemClass, + sideMenuOptions, }, reloadOnSearch: false, component: BoardPartitionedPageComponent, @@ -73,6 +81,7 @@ export const BOARDS_ROUTES:Ng2StateDeclaration[] = [ url: '', data: { baseRoute: 'boards.partitioned.show', + sideMenuOptions, }, views: { 'content-left': { component: BoardListContainerComponent }, diff --git a/frontend/src/app/features/team-planner/team-planner/add-work-packages/add-existing-pane.component.sass b/frontend/src/app/features/team-planner/team-planner/add-work-packages/add-existing-pane.component.sass index e9f779c543b5..9e8377a20154 100644 --- a/frontend/src/app/features/team-planner/team-planner/add-work-packages/add-existing-pane.component.sass +++ b/frontend/src/app/features/team-planner/team-planner/add-work-packages/add-existing-pane.component.sass @@ -4,7 +4,7 @@ display: grid grid-template-rows: 32px 1fr grid-row-gap: 10px - background: #F3F3F3 + background: var(--data-gray-color-muted) padding: 8px border: 1px solid var(--borderColor-default) diff --git a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.sass b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.sass index 90132f33f75f..5152c62a0a0f 100644 --- a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.sass +++ b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.sass @@ -53,7 +53,7 @@ $op-team-planner-resource-width: 180px &--remove-dropzone flex: 1 - border: 1px dashed #1A67A3 + border: 1px dashed var(--display-blue-bgColor-emphasis) box-sizing: border-box height: 2.25rem @@ -62,7 +62,7 @@ $op-team-planner-resource-width: 180px justify-content: center &_active - background: #D1E5F5 + background: var(--display-blue-bgColor-muted) &_forbidden border-color: var(--content-form-danger-zone-bg-color) @@ -72,12 +72,12 @@ $op-team-planner-resource-width: 180px &--dropzone-label - color: #1A67A3 + color: var(--display-blue-fgColor) &--no-data min-height: 250px padding: 2rem - background-color: #F3F3F3 + background-color: var(--data-gray-color-muted) display: flex flex-direction: column justify-content: center diff --git a/frontend/src/app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component.html b/frontend/src/app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component.html deleted file mode 100644 index 6d49c6203a3e..000000000000 --- a/frontend/src/app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - diff --git a/frontend/src/app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component.ts b/frontend/src/app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component.ts deleted file mode 100644 index a434c52cb8cc..000000000000 --- a/frontend/src/app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - ElementRef, - HostBinding, - Input, -} from '@angular/core'; -import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs'; -import { CurrentUserService } from 'core-app/core/current-user/current-user.service'; -import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; -import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; -import { I18nService } from 'core-app/core/i18n/i18n.service'; -import { BannersService } from 'core-app/core/enterprise/banners.service'; -import { map } from 'rxjs/operators'; - -export const opTeamPlannerSidemenuSelector = 'op-team-planner-sidemenu'; - -@Component({ - selector: opTeamPlannerSidemenuSelector, - templateUrl: './team-planner-sidemenu.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class TeamPlannerSidemenuComponent extends UntilDestroyedMixin { - @HostBinding('class.op-sidebar') className = true; - - @Input() menuItems:string[] = []; - - @Input() projectId:string|undefined; - - canAddTeamPlanner$ = this - .currentUserService - .hasCapabilities$( - 'team_planners/create', - this.currentProjectService.id || null, - ) - .pipe( - map((val) => val && !this.bannersService.eeShowBanners), - ); - - createButton = { - text: this.I18n.t('js.team_planner.create_label'), - title: this.I18n.t('js.team_planner.create_title'), - uiSref: 'team_planner.page.show', - uiParams: { - query_id: null, - query_props: '', - }, - }; - - constructor( - readonly elementRef:ElementRef, - readonly currentUserService:CurrentUserService, - readonly currentProjectService:CurrentProjectService, - readonly bannersService:BannersService, - readonly I18n:I18nService, - ) { - super(); - - populateInputsFromDataset(this); - } -} diff --git a/frontend/src/app/features/team-planner/team-planner/team-planner.module.ts b/frontend/src/app/features/team-planner/team-planner/team-planner.module.ts index 3d8f8316ee23..3d3d2c1e744f 100644 --- a/frontend/src/app/features/team-planner/team-planner/team-planner.module.ts +++ b/frontend/src/app/features/team-planner/team-planner/team-planner.module.ts @@ -14,7 +14,6 @@ import { TeamPlannerPageComponent } from 'core-app/features/team-planner/team-pl import { OpSharedModule } from 'core-app/shared/shared.module'; import { AddExistingPaneComponent } from './add-work-packages/add-existing-pane.component'; import { OpenprojectContentLoaderModule } from 'core-app/shared/components/op-content-loader/openproject-content-loader.module'; -import { TeamPlannerSidemenuComponent } from 'core-app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component'; import { TeamPlannerViewSelectMenuDirective } from 'core-app/features/team-planner/team-planner/view-select/view-select-menu.directive'; @NgModule({ @@ -23,7 +22,6 @@ import { TeamPlannerViewSelectMenuDirective } from 'core-app/features/team-plann TeamPlannerPageComponent, AddAssigneeComponent, AddExistingPaneComponent, - TeamPlannerSidemenuComponent, TeamPlannerViewSelectMenuDirective, ], imports: [ diff --git a/frontend/src/app/features/team-planner/team-planner/team-planner.routes.ts b/frontend/src/app/features/team-planner/team-planner/team-planner.routes.ts index 2674e488e6f7..8efb9246cb73 100644 --- a/frontend/src/app/features/team-planner/team-planner/team-planner.routes.ts +++ b/frontend/src/app/features/team-planner/team-planner/team-planner.routes.ts @@ -33,6 +33,13 @@ import { WorkPackagesBaseComponent } from 'core-app/features/work-packages/routi import { TeamPlannerPageComponent } from 'core-app/features/team-planner/team-planner/page/team-planner-page.component'; import { TeamPlannerComponent } from 'core-app/features/team-planner/team-planner/planner/team-planner.component'; +export const sidemenuId = 'team_planner_sidemenu'; +export const sideMenuOptions = { + sidemenuId, + hardReloadOnBaseRoute: true, + defaultQuery: 'new', +}; + export const TEAM_PLANNER_ROUTES:Ng2StateDeclaration[] = [ { name: 'team_planner', @@ -56,12 +63,14 @@ export const TEAM_PLANNER_ROUTES:Ng2StateDeclaration[] = [ redirectTo: 'team_planner.page.show', data: { bodyClasses: 'router--team-planner', + sideMenuOptions, }, }, { name: 'team_planner.page.show', data: { baseRoute: 'team_planner.page.show', + sideMenuOptions, }, views: { 'content-left': { component: TeamPlannerComponent }, diff --git a/frontend/src/app/features/work-packages/components/wp-buttons/wp-share-button/wp-share-button.component.ts b/frontend/src/app/features/work-packages/components/wp-buttons/wp-share-button/wp-share-button.component.ts index 417c4dd01742..a6e356bb5b4e 100644 --- a/frontend/src/app/features/work-packages/components/wp-buttons/wp-share-button/wp-share-button.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-buttons/wp-share-button/wp-share-button.component.ts @@ -57,7 +57,7 @@ export class WorkPackageShareButtonComponent extends UntilDestroyedMixin impleme shareCount$:Observable; public text = { - share: this.I18n.t('js.work_packages.sharing.share'), + share: this.I18n.t('js.sharing.share'), }; constructor( diff --git a/frontend/src/app/features/work-packages/components/wp-card-view/wp-single-card/wp-single-card.component.sass b/frontend/src/app/features/work-packages/components/wp-card-view/wp-single-card/wp-single-card.component.sass index 63fa5efb6dff..e273dd1ca386 100644 --- a/frontend/src/app/features/work-packages/components/wp-card-view/wp-single-card/wp-single-card.component.sass +++ b/frontend/src/app/features/work-packages/components/wp-card-view/wp-single-card/wp-single-card.component.sass @@ -13,6 +13,7 @@ position: relative box-shadow: 1px 1px 3px 0px var(--borderColor-default) background: var(--body-background) + color: var(--body-font-color) font-size: var(--body-font-size) &:hover @@ -36,12 +37,12 @@ box-shadow: none &_closed:not(&_selected) - background-color: var(--color-scale-gray-3) + background-color: var(--display-gray-bgColor-muted) .op-icon--wrapper - background: var(--color-scale-gray-3) + background: var(--display-gray-bgColor-muted) &:hover .op-wp-single-card--inline-buttons - box-shadow: -4px -4px 10px 4px var(--color-scale-gray-3) + box-shadow: -4px -4px 10px 4px var(--display-gray-bgColor-muted) &_horizontal height: 100% diff --git a/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts b/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts index 7c682c59487a..6652798f7b0b 100644 --- a/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts @@ -54,7 +54,7 @@ import { QueryFormResource } from 'core-app/features/hal/resources/query-form-re import { WorkPackageStatesInitializationService } from './wp-states-initialization.service'; import { WorkPackagesListInvalidQueryService } from './wp-list-invalid-query.service'; import { WorkPackagesQueryViewService } from 'core-app/features/work-packages/components/wp-list/wp-query-view.service'; -import { TurboElement } from 'core-typings/turbo'; +import { SubmenuService } from 'core-app/core/main-menu/submenu.service'; export interface QueryDefinition { queryParams:{ query_id?:string|null, query_props?:string|null }; @@ -104,6 +104,7 @@ export class WorkPackagesListService { protected wpStatesInitialization:WorkPackageStatesInitializationService, protected wpListInvalidQueryService:WorkPackagesListInvalidQueryService, protected wpQueryView:WorkPackagesQueryViewService, + protected submenuService:SubmenuService, ) { } /** @@ -462,23 +463,6 @@ export class WorkPackagesListService { } private reloadSidemenu(selectedQueryId:string|null):void { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment - const menuIdentifier:string|undefined = this.$state.current.data.sideMenuOptions?.sidemenuId; - - if (menuIdentifier) { - const menu = (document.getElementById(menuIdentifier) as HTMLElement&TurboElement); - const currentSrc = menu.getAttribute('src'); - - if (currentSrc && menu) { - const frameUrl = new URL(currentSrc); - - // Override the frame src to enforce a reload - if (selectedQueryId) { - frameUrl.search = `?query_id=${selectedQueryId}`; - } - - menu.setAttribute('src', frameUrl.href); - } - } + this.submenuService.reloadSubmenu(selectedQueryId); } } diff --git a/frontend/src/app/features/work-packages/components/wp-share-modal/wp-share.modal.html b/frontend/src/app/features/work-packages/components/wp-share-modal/wp-share.modal.html index 76cdc0ea4a02..657e673b9618 100644 --- a/frontend/src/app/features/work-packages/components/wp-share-modal/wp-share.modal.html +++ b/frontend/src/app/features/work-packages/components/wp-share-modal/wp-share.modal.html @@ -1,5 +1,5 @@
      = 0; } -export const attachmentsSelector = 'op-attachments'; - @Component({ - selector: attachmentsSelector, + selector: 'op-attachments', templateUrl: './attachments.component.html', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.html b/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.html index 02ee7b9d4a56..540d6993bf14 100644 --- a/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.html +++ b/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.html @@ -200,6 +200,7 @@ + × + × {{ item.name }} diff --git a/frontend/src/app/shared/components/fields/edit/field-types/progress-popover-edit-field.component.html b/frontend/src/app/shared/components/fields/edit/field-types/progress-popover-edit-field.component.html index 0ba8cf07be1f..9070f52f67a0 100644 --- a/frontend/src/app/shared/components/fields/edit/field-types/progress-popover-edit-field.component.html +++ b/frontend/src/app/shared/components/fields/edit/field-types/progress-popover-edit-field.component.html @@ -55,7 +55,7 @@ - + @@ -69,7 +69,7 @@ - + diff --git a/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts b/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts index a40288867158..306660270374 100644 --- a/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts +++ b/frontend/src/app/shared/components/fields/macros/attribute-value-macro.component.ts @@ -51,6 +51,8 @@ import { import { firstValueFrom } from 'rxjs'; import { ISchemaProxy } from 'core-app/features/hal/schemas/schema-proxy'; +export const ATTRIBUTE_MACRO_CLASS = 'op-attribute-value-macro'; + @Component({ templateUrl: './attribute-value-macro.html', styleUrls: ['./attribute-macro.sass'], @@ -94,8 +96,20 @@ export class AttributeValueMacroComponent implements OnInit { const model = element.dataset.model as SupportedAttributeModels; const id = element.dataset.id as string; const attributeName = element.dataset.attribute as string; + element.classList.add(ATTRIBUTE_MACRO_CLASS); + + if (this.isNestedMacro(model, id, attributeName)) { + const error = this.I18n.t('js.editor.macro.attribute_reference.nested_macro', { model, id }); + this.markError(error); + } else { + void this.loadAndRender(model, id, attributeName); + } + } - void this.loadAndRender(model, id, attributeName); + private isNestedMacro(model:SupportedAttributeModels, id:string, attributeName:string):boolean { + const element = this.elementRef.nativeElement as HTMLElement; + const parent = element.parentElement; + return !!parent?.closest(`.${ATTRIBUTE_MACRO_CLASS}[data-model="${model}"][data-id="${id}"][data-attribute="${attributeName}"]`); } private async loadAndRender(model:SupportedAttributeModels, id:string, attributeName:string):Promise { diff --git a/frontend/src/app/shared/components/grids/widgets/menu/widget-abstract-menu.component.ts b/frontend/src/app/shared/components/grids/widgets/menu/widget-abstract-menu.component.ts index 2c80f6ee68e7..e4d42f05515d 100644 --- a/frontend/src/app/shared/components/grids/widgets/menu/widget-abstract-menu.component.ts +++ b/frontend/src/app/shared/components/grids/widgets/menu/widget-abstract-menu.component.ts @@ -37,16 +37,18 @@ import { GridAreaService } from 'core-app/shared/components/grids/grid/area.serv export abstract class WidgetAbstractMenuComponent { @Input() resource:GridWidgetResource; - protected menuItemList:OpContextMenuItem[] = [this.removeItem]; - constructor(readonly injector:Injector, readonly i18n:I18nService, protected readonly remove:GridRemoveWidgetService, protected readonly layout:GridAreaService) { } - public get menuItems() { - return async () => this.menuItemList; + public get menuItemsFactory():() => Promise { + return this.buildItems.bind(this); + } + + protected async buildItems():Promise { + return [this.removeItem]; } protected get removeItem():OpContextMenuItem { diff --git a/frontend/src/app/shared/components/grids/widgets/menu/widget-menu.component.html b/frontend/src/app/shared/components/grids/widgets/menu/widget-menu.component.html index 7c5027236947..450fd0363d22 100644 --- a/frontend/src/app/shared/components/grids/widgets/menu/widget-menu.component.html +++ b/frontend/src/app/shared/components/grids/widgets/menu/widget-menu.component.html @@ -1,4 +1,4 @@ + [menuItemsFactory]="menuItemsFactory"> diff --git a/frontend/src/app/shared/components/grids/widgets/menu/wp-set-menu.component.ts b/frontend/src/app/shared/components/grids/widgets/menu/wp-set-menu.component.ts index 7678eb2c5e79..7f0b1347d30b 100644 --- a/frontend/src/app/shared/components/grids/widgets/menu/wp-set-menu.component.ts +++ b/frontend/src/app/shared/components/grids/widgets/menu/wp-set-menu.component.ts @@ -26,15 +26,20 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { - Directive, EventEmitter, Output, -} from '@angular/core'; +import { Directive, EventEmitter, Output } from '@angular/core'; import { OpModalService } from 'core-app/shared/components/modal/modal.service'; import { ComponentType } from '@angular/cdk/portal'; -import { WidgetAbstractMenuComponent } from 'core-app/shared/components/grids/widgets/menu/widget-abstract-menu.component'; -import { WpGraphConfigurationModalComponent } from 'core-app/shared/components/work-package-graphs/configuration-modal/wp-graph-configuration.modal'; -import { WpTableConfigurationModalComponent } from 'core-app/features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.modal'; +import { + WidgetAbstractMenuComponent, +} from 'core-app/shared/components/grids/widgets/menu/widget-abstract-menu.component'; +import { + WpGraphConfigurationModalComponent, +} from 'core-app/shared/components/work-package-graphs/configuration-modal/wp-graph-configuration.modal'; +import { + WpTableConfigurationModalComponent, +} from 'core-app/features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.modal'; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator'; +import { OpContextMenuItem } from 'core-app/shared/components/op-context-menu/op-context-menu.types'; @Directive() export abstract class WidgetWpSetMenuComponent extends WidgetAbstractMenuComponent { @@ -42,12 +47,20 @@ export abstract class WidgetWpSetMenuComponent extends WidgetAbstractMenuCompone @InjectField() opModalService:OpModalService; - @Output() onConfigured:EventEmitter = new EventEmitter(); + // eslint-disable-next-line @angular-eslint/no-output-on-prefix + @Output() onConfigured:EventEmitter = new EventEmitter(); + + protected async buildItems():Promise { + const items = [ + this.removeItem, + ]; - protected menuItemList = [ - this.removeItem, - this.configureItem, - ]; + if (await this.configurationAllowed()) { + items.push(this.configureItem); + } + + return items; + } protected get configureItem() { return { @@ -65,6 +78,10 @@ export abstract class WidgetWpSetMenuComponent extends WidgetAbstractMenuCompone }; } + protected configurationAllowed():Promise { + return Promise.resolve(true); + } + protected get locals() { return {}; } diff --git a/frontend/src/app/shared/components/grids/widgets/project-details/project-details-menu.component.ts b/frontend/src/app/shared/components/grids/widgets/project-details/project-details-menu.component.ts index 1085aa5705f6..6580e1d5973d 100644 --- a/frontend/src/app/shared/components/grids/widgets/project-details/project-details-menu.component.ts +++ b/frontend/src/app/shared/components/grids/widgets/project-details/project-details-menu.component.ts @@ -30,7 +30,9 @@ import { Component, OnInit, } from '@angular/core'; -import { WidgetAbstractMenuComponent } from 'core-app/shared/components/grids/widgets/menu/widget-abstract-menu.component'; +import { + WidgetAbstractMenuComponent, +} from 'core-app/shared/components/grids/widgets/menu/widget-abstract-menu.component'; import { OpContextMenuItem } from 'core-app/shared/components/op-context-menu/op-context-menu.types'; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator'; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; @@ -60,16 +62,14 @@ export class WidgetProjectDetailsMenuComponent extends WidgetAbstractMenuCompone ); } - public get menuItems() { - return async () => { - const items = [ - this.removeItem, - ]; - if (await this.capabilityPromise) { - items.push(this.projectActivityLinkItem); - } - return items; - }; + protected async buildItems():Promise { + const items = [ + this.removeItem, + ]; + if (await this.capabilityPromise) { + items.push(this.projectActivityLinkItem); + } + return items; } protected get projectActivityLinkItem():OpContextMenuItem { diff --git a/frontend/src/app/shared/components/grids/widgets/time-entries/current-user/time-entries-current-user-menu.component.ts b/frontend/src/app/shared/components/grids/widgets/time-entries/current-user/time-entries-current-user-menu.component.ts index 2a5e6c0cde2e..3803991f8b1e 100644 --- a/frontend/src/app/shared/components/grids/widgets/time-entries/current-user/time-entries-current-user-menu.component.ts +++ b/frontend/src/app/shared/components/grids/widgets/time-entries/current-user/time-entries-current-user-menu.component.ts @@ -30,9 +30,14 @@ import { Component, EventEmitter, Output, } from '@angular/core'; import { OpModalService } from 'core-app/shared/components/modal/modal.service'; -import { WidgetAbstractMenuComponent } from 'core-app/shared/components/grids/widgets/menu/widget-abstract-menu.component'; -import { TimeEntriesCurrentUserConfigurationModalComponent } from 'core-app/shared/components/grids/widgets/time-entries/current-user/configuration-modal/configuration.modal'; +import { + WidgetAbstractMenuComponent, +} from 'core-app/shared/components/grids/widgets/menu/widget-abstract-menu.component'; +import { + TimeEntriesCurrentUserConfigurationModalComponent, +} from 'core-app/shared/components/grids/widgets/time-entries/current-user/configuration-modal/configuration.modal'; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator'; +import { OpContextMenuItem } from 'core-app/shared/components/op-context-menu/op-context-menu.types'; @Component({ selector: 'widget-time-entries-current-user-menu', @@ -43,10 +48,12 @@ export class WidgetTimeEntriesCurrentUserMenuComponent extends WidgetAbstractMen @Output() onConfigured:EventEmitter = new EventEmitter(); - protected menuItemList = [ - this.removeItem, - this.configureItem, - ]; + protected async buildItems():Promise { + return [ + this.removeItem, + this.configureItem, + ]; + } protected get configureItem() { return { diff --git a/frontend/src/app/shared/components/grids/widgets/time-entries/list/time-entries-list.component.ts b/frontend/src/app/shared/components/grids/widgets/time-entries/list/time-entries-list.component.ts index 27bdcddba5ae..d7ea932a681d 100644 --- a/frontend/src/app/shared/components/grids/widgets/time-entries/list/time-entries-list.component.ts +++ b/frontend/src/app/shared/components/grids/widgets/time-entries/list/time-entries-list.component.ts @@ -31,6 +31,7 @@ export abstract class WidgetTimeEntriesListComponent extends AbstractWidgetCompo title: this.i18n.t('js.modals.destroy_time_entry.title'), }, noResults: this.i18n.t('js.grid.widgets.time_entries_list.no_results'), + placeholder: this.i18n.t('js.placeholders.default'), }; public entries:TimeEntryResource[] = []; @@ -84,7 +85,7 @@ export abstract class WidgetTimeEntriesListComponent extends AbstractWidgetCompo } public activityName(entry:TimeEntryResource):string { - return entry.activity.name; + return entry.activity ? entry.activity.name : this.text.placeholder; } public projectName(entry:TimeEntryResource):string { diff --git a/frontend/src/app/shared/components/grids/widgets/wp-table/wp-table-menu.component.ts b/frontend/src/app/shared/components/grids/widgets/wp-table/wp-table-menu.component.ts index 518b7e786976..a5f36239088e 100644 --- a/frontend/src/app/shared/components/grids/widgets/wp-table/wp-table-menu.component.ts +++ b/frontend/src/app/shared/components/grids/widgets/wp-table/wp-table-menu.component.ts @@ -27,13 +27,26 @@ //++ import { Component } from '@angular/core'; -import { WpTableConfigurationModalComponent } from 'core-app/features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.modal'; +import { + WpTableConfigurationModalComponent, +} from 'core-app/features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.modal'; import { WidgetWpSetMenuComponent } from 'core-app/shared/components/grids/widgets/menu/wp-set-menu.component'; +import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator'; +import { CurrentUserService } from 'core-app/core/current-user/current-user.service'; +import { firstValueFrom } from 'rxjs'; @Component({ selector: 'widget-wp-table-menu', templateUrl: '../menu/widget-menu.component.html', }) export class WidgetWpTableMenuComponent extends WidgetWpSetMenuComponent { + @InjectField() currentUser:CurrentUserService; + protected configurationComponent = WpTableConfigurationModalComponent; + + protected configurationAllowed():Promise { + return firstValueFrom( + this.currentUser.hasCapabilities$('queries/create', null), + ); + } } diff --git a/frontend/src/app/shared/components/op-content-loader/op-content-loader.component.html b/frontend/src/app/shared/components/op-content-loader/op-content-loader.component.html index 05ae7070a636..af1683daeb27 100644 --- a/frontend/src/app/shared/components/op-content-loader/op-content-loader.component.html +++ b/frontend/src/app/shared/components/op-content-loader/op-content-loader.component.html @@ -1,6 +1,6 @@ Promise; protected async open(evt:JQuery.TriggeredEvent) { this.items = await this.buildItems(); @@ -78,8 +80,8 @@ export class IconTriggeredContextMenuComponent extends OpContextMenuTrigger { const items:OpContextMenuItem[] = []; // Add action specific menu entries - if (this.menuItems) { - const additional = await this.menuItems(); + if (this.menuItemsFactory) { + const additional = await this.menuItemsFactory(); return items.concat(additional); } diff --git a/frontend/src/app/shared/components/op-view-select/op-static-queries.service.ts b/frontend/src/app/shared/components/op-view-select/op-static-queries.service.ts index 75d16b7a9377..51566ed8c9d0 100644 --- a/frontend/src/app/shared/components/op-view-select/op-static-queries.service.ts +++ b/frontend/src/app/shared/components/op-view-select/op-static-queries.service.ts @@ -93,7 +93,7 @@ export class StaticQueriesService { if (this.$state.params.name) { const nameKey = this.$state.params.name as string; - return this.I18n.t(`js.queries.${nameKey}`); + return this.I18n.t(`js.work_packages.default_queries.${nameKey}`); } } @@ -143,33 +143,6 @@ export class StaticQueriesService { }, view: 'WorkPackagesTable', }, - { - title: this.text.all_open, - uiSref: 'bim.partitioned.list', - uiParams: { - query_id: undefined, - query_props: '{"c":["id","subject","bcfThumbnail","type","status","assignee","updatedAt"],"t":"id:desc"}', - }, - view: 'Bim', - }, - { - title: this.text.latest_activity, - uiSref: 'bim.partitioned.list', - uiParams: { - query_id: undefined, - query_props: '{"c":["id","subject","bcfThumbnail","type","status","assignee","updatedAt"],"t":"updatedAt:desc","f":[{"n":"status","o":"o","v":[]}]}', - }, - view: 'Bim', - }, - { - title: this.text.recently_created, - uiSref: 'bim.partitioned.list', - uiParams: { - query_id: undefined, - query_props: '{"c":["id","subject","bcfThumbnail","type","status","assignee","createdAt"],"t":"createdAt:desc","f":[{"n":"status","o":"o","v":[]}]}', - }, - view: 'Bim', - }, ]; const projectIdentifier = this.CurrentProject.identifier; @@ -237,24 +210,6 @@ export class StaticQueriesService { isEnterprise: true, ...this.eeGuardedShareWithMeRoute, }, - { - title: this.text.created_by_me, - uiSref: 'bim.partitioned.list', - uiParams: { - query_id: undefined, - query_props: '{"c":["id","subject","bcfThumbnail","type","status","assignee","updatedAt"],"t":"id:desc","f":[{"n":"status","o":"o","v":[]},{"n":"author","o":"=","v":["me"]}]}', - }, - view: 'Bim', - }, - { - title: this.text.assigned_to_me, - uiSref: 'bim.partitioned.list', - uiParams: { - query_id: undefined, - query_props: '{"c":["id","subject","bcfThumbnail","type","status","author","updatedAt"],"t":"id:desc","f":[{"n":"status","o":"o","v":[]},{"n":"assigneeOrGroup","o":"=","v":["me"]}]}', - }, - view: 'Bim', - }, ]; } diff --git a/frontend/src/app/spot/styles/sass/components/form-field.sass b/frontend/src/app/spot/styles/sass/components/form-field.sass index 75ad7d483dbe..e8c73d300eed 100644 --- a/frontend/src/app/spot/styles/sass/components/form-field.sass +++ b/frontend/src/app/spot/styles/sass/components/form-field.sass @@ -54,7 +54,7 @@ &--error @include spot-caption - color: $spot-color-danger-dark + color: var(--control-borderColor-danger) margin-bottom: $spot-spacing-0_25 &--label, diff --git a/frontend/src/assets/sass/backlogs/_taskboard.sass b/frontend/src/assets/sass/backlogs/_taskboard.sass index b889c60dcef7..03f2a267af9e 100644 --- a/frontend/src/assets/sass/backlogs/_taskboard.sass +++ b/frontend/src/assets/sass/backlogs/_taskboard.sass @@ -114,7 +114,7 @@ cursor: pointer background-color: var(--highlight-neutral-bgColor) .story, .label_sprint_impediments - background-color: var(--color-scale-yellow-0) + background-color: var(--display-lemon-bgColor-muted) color: var(--fgColor-muted) border: none display: block @@ -135,7 +135,7 @@ text-decoration: line-through .work_package, .placeholder background-color: #AFAFAF - color: var(--color-scale-black) + color: var(--color-ansi-black) border: none cursor: move display: block diff --git a/frontend/src/global_styles/content/_activity_list.sass b/frontend/src/global_styles/content/_activity_list.sass index b61ed3e9b622..30e78af54187 100644 --- a/frontend/src/global_styles/content/_activity_list.sass +++ b/frontend/src/global_styles/content/_activity_list.sass @@ -13,6 +13,7 @@ &-title @include spot-body-small (bold) + color: var(--body-font-color) &-subtitle @include spot-caption diff --git a/frontend/src/global_styles/content/_autocomplete.sass b/frontend/src/global_styles/content/_autocomplete.sass index 4f54567e3245..6a6fde2eca44 100644 --- a/frontend/src/global_styles/content/_autocomplete.sass +++ b/frontend/src/global_styles/content/_autocomplete.sass @@ -111,6 +111,9 @@ div.autocomplete color: var(--body-font-color) !important border-color: var(--borderColor-default) !important +.ng-select-container + background-color: transparent !important + .ng-select .ng-arrow-wrapper .ng-arrow border-color: var(--fgColor-muted) transparent transparent diff --git a/frontend/src/global_styles/content/_datepicker.sass b/frontend/src/global_styles/content/_datepicker.sass index 25be32f39bb9..cd9944fff5d4 100644 --- a/frontend/src/global_styles/content/_datepicker.sass +++ b/frontend/src/global_styles/content/_datepicker.sass @@ -152,8 +152,8 @@ $datepicker--selected-border-radius: 5px @include non-working-day_enabled &.today:not(.selected, .inRange, .startRange, .endRange) - background: var(--color-scale-yellow-0) - border-color: var(--color-scale-yellow-0) + background: var(--label-yellow-bgColor-active) + border-color: var(--label-yellow-bgColor-active) border-radius: 0 color: var(--fgColor-muted) @@ -168,7 +168,7 @@ $datepicker--selected-border-radius: 5px &.endRange background: var(--control-checked-color) border-color: var(--control-checked-color) - color: var(--color-scale-white) + color: var(--fgColor-white) &:hover background: var(--control-checked-color--major2) @@ -227,8 +227,8 @@ $datepicker--selected-border-radius: 5px &.today color: var(--fgColor-muted) - background: var(--color-scale-yellow-0) - border-color: var(--color-scale-yellow-0) + background: var(--label-yellow-bgColor-active) + border-color: var(--label-yellow-bgColor-active) .flatpickr-calendar.flatpickr-container-suppress-hover .flatpickr-day diff --git a/frontend/src/global_styles/content/_grid.sass b/frontend/src/global_styles/content/_grid.sass index 2f69daa35487..e4c66a85f491 100644 --- a/frontend/src/global_styles/content/_grid.sass +++ b/frontend/src/global_styles/content/_grid.sass @@ -200,7 +200,7 @@ body.widget-grid-layout .grid--widget-add padding: 15px - background-color: var(--gray) + background-color: var(--bgColor-neutral-emphasis) border-radius: 50% opacity: 0 diff --git a/frontend/src/global_styles/content/_hide_section.sass b/frontend/src/global_styles/content/_hide_section.sass index be57d18bfbc2..5073ddd7b5ae 100644 --- a/frontend/src/global_styles/content/_hide_section.sass +++ b/frontend/src/global_styles/content/_hide_section.sass @@ -31,7 +31,7 @@ section.hide-section flex-wrap: nowrap padding: 0.5rem 0 &:hover - background-color: #f8f8f8 + background-color: var(--control-transparent-bgColor-hover) .form--field-container flex-shrink: 1 diff --git a/frontend/src/global_styles/content/_progress_bar.sass b/frontend/src/global_styles/content/_progress_bar.sass index 7d9ffcd98ce2..c644b5aa842c 100644 --- a/frontend/src/global_styles/content/_progress_bar.sass +++ b/frontend/src/global_styles/content/_progress_bar.sass @@ -41,9 +41,9 @@ float: left height: 100% &.done - background: var(--color-scale-green-1) none repeat scroll 0% + background: var(--display-green-bgColor-muted) none repeat scroll 0% &.closed - background: var(--color-scale-red-1) none repeat scroll 0% + background: var(--display-green-bgColor-emphasis) none repeat scroll 0% .progress-bar-legend font-size: 80% diff --git a/frontend/src/global_styles/content/modules/_costs.sass b/frontend/src/global_styles/content/modules/_costs.sass index f29563af3558..872831293942 100644 --- a/frontend/src/global_styles/content/modules/_costs.sass +++ b/frontend/src/global_styles/content/modules/_costs.sass @@ -30,7 +30,7 @@ table.list.members margin-bottom: 0 .progress-bar .inner-progress.done - background-color: #E1B9B9 + background-color: var(--display-red-bgColor-muted) .budget-row-template, .subform-row-template diff --git a/frontend/src/global_styles/content/work_packages/_table_inline_create.sass b/frontend/src/global_styles/content/work_packages/_table_inline_create.sass index ebd2df56eb03..f41a8bf38963 100644 --- a/frontend/src/global_styles/content/work_packages/_table_inline_create.sass +++ b/frontend/src/global_styles/content/work_packages/_table_inline_create.sass @@ -14,10 +14,10 @@ line-height: 1.6 .wp-inline-create-row - background: #BEF3CA + background: var(--display-green-bgColor-muted) &:hover - background: darken(#BEF3CA, 5%) !important + background: var(--display-pine-bgColor-muted) .wp-table--cancel-create-td text-align: center !important diff --git a/frontend/src/global_styles/content/work_packages/timelines/_timelines_header.sass b/frontend/src/global_styles/content/work_packages/timelines/_timelines_header.sass index b359d6d8f0b4..4f5691037e80 100644 --- a/frontend/src/global_styles/content/work_packages/timelines/_timelines_header.sass +++ b/frontend/src/global_styles/content/work_packages/timelines/_timelines_header.sass @@ -9,6 +9,7 @@ wp-timeline-header z-index: 2 .wp-timeline--header-element + background: var(--body-background) position: absolute height: 10px width: 10px diff --git a/frontend/src/global_styles/layout/_grid.sass b/frontend/src/global_styles/layout/_grid.sass index 860c70b4a41b..98837a8565f7 100644 --- a/frontend/src/global_styles/layout/_grid.sass +++ b/frontend/src/global_styles/layout/_grid.sass @@ -325,10 +325,3 @@ $breakpoint-map: (small: sm, medium: md, large: lg, xlarge: xl) @mixin grid-visible-overflow overflow: visible overflow-y: visible - -// Allow to show overflow for drop-downs -.grid-content, -.grid-block - - &.-visible-overflow - overflow: visible diff --git a/frontend/src/global_styles/layout/_main_menu.sass b/frontend/src/global_styles/layout/_main_menu.sass index 6bb12990f9ec..e513a449a2ac 100644 --- a/frontend/src/global_styles/layout/_main_menu.sass +++ b/frontend/src/global_styles/layout/_main_menu.sass @@ -348,6 +348,8 @@ a.main-menu--parent-node .main-menu--navigation-toggler position: relative + // Needed for Safari due to the fixed position of the parent element + -webkit-transform: translateZ(0) cursor: pointer margin-left: -0.75rem color: var(--main-menu-font-color) diff --git a/frontend/src/global_styles/openproject/_forms.sass b/frontend/src/global_styles/openproject/_forms.sass index e2f1c1f4ccc4..f6010ecfb5a2 100644 --- a/frontend/src/global_styles/openproject/_forms.sass +++ b/frontend/src/global_styles/openproject/_forms.sass @@ -51,11 +51,10 @@ $input-border-focus: 1px solid #999 !default // Labels $form-label-margin: 0.5rem !default -$form-label-color: #333 !default // Inline labels -$inlinelabel-color: #333 !default -$inlinelabel-background: #eee !default +$inlinelabel-color: var(--button--font-color) !default +$inlinelabel-background: var(--button--background-color) !default $inlinelabel-border: $input-border !default // Select menus diff --git a/frontend/src/global_styles/openproject/_variable_defaults.scss b/frontend/src/global_styles/openproject/_variable_defaults.scss index 6d2228c9a377..bcae407941ca 100644 --- a/frontend/src/global_styles/openproject/_variable_defaults.scss +++ b/frontend/src/global_styles/openproject/_variable_defaults.scss @@ -115,8 +115,8 @@ --user-avatar-border-radius: 50%; --inplace-edit--border-color: #ddd; --inplace-edit--color--very-dark: #cacaca; - --inplace-edit--color--disabled: #4d4d4d; - --inplace-edit--bg-color--disabled: #eee; + --inplace-edit--color--disabled: var(--control-fgColor-disabled); + --inplace-edit--bg-color--disabled: var(--control-bgColor-disabled); --table-border-color: #E7E7E7; --table-row-highlighting-outline-color: #00A6FF; --button--font-color: #222222; diff --git a/frontend/src/global_styles/primer/_icons.sass b/frontend/src/global_styles/primer/_icons.sass index 78048aee5425..a0b34ef084a5 100644 --- a/frontend/src/global_styles/primer/_icons.sass +++ b/frontend/src/global_styles/primer/_icons.sass @@ -30,4 +30,4 @@ .op-primer--star-icon, .Button--invisible.Button--iconOnly.op-primer--star-icon svg - color: var(--color-scale-yellow-2) !important + color: var(--button-star-iconColor) !important diff --git a/frontend/src/global_styles/primer/_overrides.sass b/frontend/src/global_styles/primer/_overrides.sass index 9c22086bd427..7575a2f4eb25 100644 --- a/frontend/src/global_styles/primer/_overrides.sass +++ b/frontend/src/global_styles/primer/_overrides.sass @@ -67,16 +67,9 @@ page-header // For some specific cases like progress form in small screens, // we need to change the direction of elements to column, -// therefore elements are placed below each other. -// -// body needed for higher specificity compared to .FormControl-horizontalGroup--center-aligned -body .FormControl-horizontalGroup--sm-vertical +// therefore elements are placed below each other +.FormControl-horizontalGroup--sm-vertical @media screen and (max-width: $breakpoint-sm) .FormControl-horizontalGroup flex-direction: column row-gap: 1rem - align-items: normal - -.FormControl-horizontalGroup--center-aligned - .FormControl-horizontalGroup - align-items: center diff --git a/frontend/src/stimulus/controllers/dynamic/work-packages/share/bulk-selection.controller.ts b/frontend/src/stimulus/controllers/dynamic/shares/bulk-selection.controller.ts similarity index 94% rename from frontend/src/stimulus/controllers/dynamic/work-packages/share/bulk-selection.controller.ts rename to frontend/src/stimulus/controllers/dynamic/shares/bulk-selection.controller.ts index f6d119154bc5..a82042673b8a 100644 --- a/frontend/src/stimulus/controllers/dynamic/work-packages/share/bulk-selection.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/shares/bulk-selection.controller.ts @@ -32,7 +32,7 @@ import { Controller } from '@hotwired/stimulus'; export default class BulkSelectionController extends Controller { static values = { - bulkUpdateRoleLabel: { type: String, default: I18n.t('js.work_packages.sharing.selection.mixed') }, + bulkUpdateRoleLabel: { type: String, default: I18n.t('js.sharing.selection.mixed') }, }; declare bulkUpdateRoleLabelValue:string; @@ -152,7 +152,7 @@ export default class BulkSelectionController extends Controller { private updateBulkUpdateRoleLabelValue() { if (new Set(this.selectedPermissions).size > 1) { - this.bulkUpdateRoleLabelValue = I18n.t('js.work_packages.sharing.selection.mixed'); + this.bulkUpdateRoleLabelValue = I18n.t('js.sharing.selection.mixed'); this.bulkPermissionButtons.forEach((button) => button.setAttribute('aria-checked', 'false')); } else { this.bulkUpdateRoleLabelValue = this.selectedPermissions[0]; @@ -180,7 +180,7 @@ export default class BulkSelectionController extends Controller { } private showSelectedCounter() { - this.selectedCounterTarget.textContent = I18n.t('js.work_packages.sharing.selected_count', { count: this.checked.length }); + this.selectedCounterTarget.textContent = I18n.t('js.sharing.selected_count', { count: this.checked.length }); this.sharedCounterTarget.setAttribute('hidden', 'true'); this.selectedCounterTarget.removeAttribute('hidden'); } @@ -201,7 +201,7 @@ export default class BulkSelectionController extends Controller { hiddenInput.type = 'hidden'; hiddenInput.name = 'share_ids[]'; hiddenInput.value = checkbox.value; - hiddenInput.setAttribute('data-work-packages--share--bulk-selection-target', 'hiddenShare'); + hiddenInput.setAttribute('data-shares--bulk-selection-target', 'hiddenShare'); return hiddenInput; }); diff --git a/frontend/src/stimulus/controllers/dynamic/work-packages/share/user-selected.controller.ts b/frontend/src/stimulus/controllers/dynamic/shares/user-selected.controller.ts similarity index 100% rename from frontend/src/stimulus/controllers/dynamic/work-packages/share/user-selected.controller.ts rename to frontend/src/stimulus/controllers/dynamic/shares/user-selected.controller.ts diff --git a/lib/api/root.rb b/lib/api/root.rb index 2fa907550dab..fe64cde5e91c 100644 --- a/lib/api/root.rb +++ b/lib/api/root.rb @@ -35,7 +35,7 @@ class Root < ::API::RootAPI parser :json, API::V3::Parser.new - error_representer ::API::V3::Errors::ErrorRepresenter, "hal+json" + error_representer ::API::V3::Errors::ErrorRepresenter, "application/hal+json; charset=utf-8" authentication_scope OpenProject::Authentication::Scope::API_V3 OpenProject::Authentication.handle_failure(scope: API_V3) do |warden, _opts| @@ -44,7 +44,7 @@ class Root < ::API::RootAPI api_error = ::API::Errors::Unauthenticated.new error_message representer = ::API::V3::Errors::ErrorRepresenter.new api_error - e.error_response status: 401, message: representer.to_json, headers: warden.headers, log: false + e.error! representer.to_json, 401, warden.headers end version "v3", using: :path do diff --git a/lib/api/utilities/grape_helper.rb b/lib/api/utilities/grape_helper.rb index 26a364e5bd62..270e6227eb3c 100644 --- a/lib/api/utilities/grape_helper.rb +++ b/lib/api/utilities/grape_helper.rb @@ -38,6 +38,10 @@ def initialize(env) @env = env @options = {} end + + def error!(message, status = nil, headers = nil, backtrace = nil, original_exception = nil) + super + end end def grape_error_for(env, api) @@ -67,7 +71,7 @@ def default_error_response(headers, log) original_exception = $! representer = error_representer.new e resp_headers = instance_exec &headers - env["api.format"] = error_content_type + resp_headers["Content-Type"] = error_content_type if log == true OpenProject.logger.error original_exception, reference: :APIv3 @@ -75,7 +79,7 @@ def default_error_response(headers, log) log.call(original_exception) end - error_response status: e.code, message: representer.to_json, headers: resp_headers + error!(representer.to_json, e.code, resp_headers) } end end diff --git a/lib/api/v3/news/news_api.rb b/lib/api/v3/news/news_api.rb index b9012762f6ad..d47ba3fb1264 100644 --- a/lib/api/v3/news/news_api.rb +++ b/lib/api/v3/news/news_api.rb @@ -36,6 +36,10 @@ class NewsAPI < ::API::OpenProjectAPI self_path: :newses) .mount + post &::API::V3::Utilities::Endpoints::Create + .new(model: News) + .mount + route_param :id, type: Integer, desc: "News ID" do after_validation do @news = ::News @@ -46,6 +50,8 @@ class NewsAPI < ::API::OpenProjectAPI get &::API::V3::Utilities::Endpoints::Show .new(model: ::News) .mount + patch &::API::V3::Utilities::Endpoints::Update.new(model: ::News).mount + delete &::API::V3::Utilities::Endpoints::Delete.new(model: ::News, success_status: 204).mount end end end diff --git a/lib/api/v3/news/news_payload_representer.rb b/lib/api/v3/news/news_payload_representer.rb new file mode 100644 index 000000000000..56f9184a9aab --- /dev/null +++ b/lib/api/v3/news/news_payload_representer.rb @@ -0,0 +1,37 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module API + module V3 + module News + class NewsPayloadRepresenter < NewsRepresenter + include ::API::Utilities::PayloadRepresenter + end + end + end +end diff --git a/lib/api/v3/news/news_representer.rb b/lib/api/v3/news/news_representer.rb index 968322c832d7..d917b86caa60 100644 --- a/lib/api/v3/news/news_representer.rb +++ b/lib/api/v3/news/news_representer.rb @@ -63,6 +63,22 @@ class NewsRepresenter < ::API::Decorators::Single v3_path: :user, representer: ::API::V3::Users::UserRepresenter + link :updateImmediately, + cache_if: -> { current_user.allowed_in_project?(:manage_news, represented.project) } do + { + href: api_v3_paths.news(represented.id), + method: :patch + } + end + + link :delete, + cache_if: -> { current_user.allowed_in_project?(:manage_news, represented.project) } do + { + href: api_v3_paths.news(represented.id), + method: :delete + } + end + def _type "News" end diff --git a/lib/api/v3/root.rb b/lib/api/v3/root.rb index 1d2e22b576cf..8deeb4b61d64 100644 --- a/lib/api/v3/root.rb +++ b/lib/api/v3/root.rb @@ -101,6 +101,12 @@ class Root < ::API::OpenProjectAPI API::OpenAPI.spec.to_yaml end + + # Catch all unknown routes (therefore have it at the end of the file) + # and return a properly formatted 404 error. + route :any, "*path" do + raise API::Errors::NotFound + end end end end diff --git a/lib/api/v3/utilities/path_helper.rb b/lib/api/v3/utilities/path_helper.rb index 75ada6df0744..bfe5c673ad5c 100644 --- a/lib/api/v3/utilities/path_helper.rb +++ b/lib/api/v3/utilities/path_helper.rb @@ -243,6 +243,7 @@ def self.memberships_available_projects show :message index :newses, :news + show :news def self.news(id) "#{newses}/#{id}" diff --git a/lib/open_project/access_control/permission.rb b/lib/open_project/access_control/permission.rb index 73157c9e1b3a..6f7f9027e58d 100644 --- a/lib/open_project/access_control/permission.rb +++ b/lib/open_project/access_control/permission.rb @@ -89,7 +89,28 @@ def project_query? end def permissible_on?(context_type) - @permissible_on.include?(context_type) + # Sometimes the context_type passed in is a decorated object. + # Most of the times, this would then be an 'EagerLoadingWrapper' instance. + # We need to unwrap the object to get the actual object. + # Checking for `context_type.is_a?(SimpleDelegator)` fails for unknown reasons. + context_type = context_type.__getobj__ if context_type.class.ancestors.include?(SimpleDelegator) + + context_symbol = case context_type + when WorkPackage + :work_package + when Project + :project + when ::ProjectQuery + :project_query + when Symbol + context_type + when nil + :global + else + raise "Unknown context: #{context_type}" + end + + @permissible_on.include?(context_symbol) end def grant_to_admin? diff --git a/lib/open_project/patches/lookbook_tree_node_inflector.rb b/lib/open_project/patches/lookbook_tree_node_inflector.rb index 2c62463e921d..6a5cb9cff750 100644 --- a/lib/open_project/patches/lookbook_tree_node_inflector.rb +++ b/lib/open_project/patches/lookbook_tree_node_inflector.rb @@ -41,7 +41,7 @@ def label end if Rails.env.development? - OpenProject::Patches.patch_gem_version "lookbook", "2.3.1" do + OpenProject::Patches.patch_gem_version "lookbook", "2.3.2" do Lookbook::TreeNode.prepend OpenProject::Patches::LookbookTreeNodeInflector end end diff --git a/lib/primer/open_project/forms/dsl/project_autocompleter_input.rb b/lib/primer/open_project/forms/dsl/project_autocompleter_input.rb index 5b58c0ba542f..4c43d5fcd143 100644 --- a/lib/primer/open_project/forms/dsl/project_autocompleter_input.rb +++ b/lib/primer/open_project/forms/dsl/project_autocompleter_input.rb @@ -9,7 +9,7 @@ def derive_autocompleter_options(options) options.reverse_merge!( component: "opce-project-autocompleter", defaultData: false, - filters: [{ name: 'active', operator: '=', values: ['t'] }], + filters: [{ name: "active", operator: "=", values: ["t"] }] ) if options[:disabledProjects] 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 51ae9205edfd..35f9c13a5f3e 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 @@ -42,7 +42,7 @@ def association_changes(original, changed, *) get_association_changes(original, changed, *) end - def association_changes_multiple_attributes(original, changed, association, association_name, key, values) + def association_changes_multiple_attributes(original, changed, association, association_name, key, values) list = {} values.each do |value| list.store(value, get_association_changes(original, changed, association, association_name, key, value)) @@ -69,7 +69,7 @@ def normalize_newlines(data) def no_nil_to_empty_strings?(normalized_old_data, attribute, new_value) old_value = normalized_old_data[attribute] - new_value != old_value && ([new_value, old_value] - ['', nil]).present? + new_value != old_value && ([new_value, old_value] - ["", nil]).present? end def journaled_attributes(object) @@ -133,7 +133,7 @@ def select_and_combine_journals(journals, id, key, value) if selected_journals.empty? nil else - selected_journals.sort.join(',') + selected_journals.sort.join(",") end end end diff --git a/lib_static/plugins/acts_as_journalized/lib/acts_as_journalized.rb b/lib_static/plugins/acts_as_journalized/lib/acts_as_journalized.rb index 0c5862d6362c..016922887d68 100644 --- a/lib_static/plugins/acts_as_journalized/lib/acts_as_journalized.rb +++ b/lib_static/plugins/acts_as_journalized/lib/acts_as_journalized.rb @@ -45,14 +45,14 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require 'journal_changes' -require 'journal_formatter' -require 'cause_of_change' +require "journal_changes" +require "journal_formatter" +require "cause_of_change" module Acts end -Dir[File.expand_path('acts/journalized/*.rb', __dir__)].sort.each { |f| require f } +Dir[File.expand_path("acts/journalized/*.rb", __dir__)].sort.each { |f| require f } module Acts module Journalized diff --git a/lib_static/plugins/acts_as_journalized/lib/cause_of_change.rb b/lib_static/plugins/acts_as_journalized/lib/cause_of_change.rb index c7294d844ce5..0202fe10dead 100644 --- a/lib_static/plugins/acts_as_journalized/lib/cause_of_change.rb +++ b/lib_static/plugins/acts_as_journalized/lib/cause_of_change.rb @@ -45,8 +45,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require_relative 'cause_of_change/base' -require_relative 'cause_of_change/no_cause' +require_relative "cause_of_change/base" +require_relative "cause_of_change/no_cause" module CauseOfChange end diff --git a/lookbook/previews/open_project/common/submenu_component_preview.rb b/lookbook/previews/open_project/common/submenu_component_preview.rb index b8d4bce36f26..b64d39f2c7b9 100644 --- a/lookbook/previews/open_project/common/submenu_component_preview.rb +++ b/lookbook/previews/open_project/common/submenu_component_preview.rb @@ -21,7 +21,7 @@ def searchable def with_create_button render_with_template(template: "open_project/common/submenu_preview/playground", locals: { sidebar_menu_items: menu_items, searchable: true, - create_btn_options: { href: "/#", text: "User" } }) + create_btn_options: { href: "/#", module_key: "user" } }) end private diff --git a/lookbook/previews/open_project/filter/filter_button_component_preview.rb b/lookbook/previews/open_project/filter/filter_button_component_preview.rb index b16455531d6b..2f77f67cfe4f 100644 --- a/lookbook/previews/open_project/filter/filter_button_component_preview.rb +++ b/lookbook/previews/open_project/filter/filter_button_component_preview.rb @@ -3,7 +3,7 @@ module Filter # @logical_path OpenProject/Filter class FilterButtonComponentPreview < Lookbook::Preview def default - @query = Queries::Projects::ProjectQuery.new + @query = ProjectQuery.new render(::Filter::FilterButtonComponent.new(query: @query)) end @@ -12,7 +12,7 @@ def default # Just register the controller in a container around both elements. # Unfortunately, stimulus controllers do not work in our lookbook as of now, so you will see no effect. def filter_section_toggle - @query = Queries::Projects::ProjectQuery.new + @query = ProjectQuery.new render_with_template(locals: { query: @query }) end end diff --git a/lookbook/previews/open_project/filter/filters_component_preview.rb b/lookbook/previews/open_project/filter/filters_component_preview.rb index 13646dc4fcb7..eaa75f1f9101 100644 --- a/lookbook/previews/open_project/filter/filters_component_preview.rb +++ b/lookbook/previews/open_project/filter/filters_component_preview.rb @@ -3,7 +3,7 @@ module Filter # @logical_path OpenProject/Filter class FiltersComponentPreview < Lookbook::Preview def default - @query = Queries::Projects::ProjectQuery.new + @query = ProjectQuery.new render(Projects::ProjectsFiltersComponent.new(query: @query)) end end diff --git a/lookbook/previews/open_project/progress/modal_preview/status_based.html.erb b/lookbook/previews/open_project/progress/modal_preview/status_based.html.erb index 0964ac897048..1ffc6c8db93f 100644 --- a/lookbook/previews/open_project/progress/modal_preview/status_based.html.erb +++ b/lookbook/previews/open_project/progress/modal_preview/status_based.html.erb @@ -4,7 +4,7 @@ - -
        - <% involvement_sidebar_menu_items.each do |menu_item| %> -
      • - <%= menu_item %> -
      • - <% end %> -
      -
      -
      -
    - - -
    diff --git a/modules/meeting/app/views/meetings/menus/_menu.html.erb b/modules/meeting/app/views/meetings/menus/_menu.html.erb new file mode 100644 index 000000000000..74bacf441dd4 --- /dev/null +++ b/modules/meeting/app/views/meetings/menus/_menu.html.erb @@ -0,0 +1,5 @@ + <%= turbo_frame_tag "meeting_sidemenu", + src: @project.present? ? menu_project_meetings_path(@project, **params.permit(:filters, :sort)) : meetings_menu_path(**params.permit(:filters, :sort)), + target: '_top', + data: { turbo: false }, + loading: :lazy %> diff --git a/modules/meeting/app/views/meetings/menus/show.html.erb b/modules/meeting/app/views/meetings/menus/show.html.erb new file mode 100644 index 000000000000..84fcb51f5019 --- /dev/null +++ b/modules/meeting/app/views/meetings/menus/show.html.erb @@ -0,0 +1,4 @@ +<%= turbo_frame_tag "meeting_sidemenu" do %> + <%= render OpenProject::Common::SubmenuComponent.new(sidebar_menu_items: @submenu_menu_items, + create_btn_options: @create_btn_options) %> +<% end %> diff --git a/modules/meeting/config/locales/crowdin/hu.yml b/modules/meeting/config/locales/crowdin/hu.yml index 79445029b3e2..d99b0f01f3bc 100644 --- a/modules/meeting/config/locales/crowdin/hu.yml +++ b/modules/meeting/config/locales/crowdin/hu.yml @@ -40,16 +40,16 @@ hu: participants_invited: "Meghívottak" project: "Projekt" start_date: "dátum" - start_time: "Start time" - start_time_hour: "Start time" + start_time: "Kezdés" + start_time_hour: "Kezdés" meeting_agenda_item: title: "Cím" author: "Szerző" - duration_in_minutes: "min" + duration_in_minutes: "Perc" description: "Jegyzet" - presenter: "Presenter" + presenter: "Bemutató" meeting_section: - title: "Title" + title: "Cím" errors: messages: invalid_time_format: "nem egy érvényes időpont. Előírt formátum: óó:pp" @@ -58,23 +58,23 @@ hu: meeting_agenda_item: "Napirendi pont" meeting_agenda: "Napirend" meeting_minutes: "Jegyzőkönyv" - meeting_section: "Section" + meeting_section: "Szekció " activity: filter: meeting: "Megbeszélések" item: meeting_agenda_item: duration: - added: "set to %{value}" - added_html: "set to %{value}" - removed: "removed" - updated: "changed from %{old_value} to %{value}" - updated_html: "changed from %{old_value} to %{value}" + added: "Átállítás %{value}-ra/-re" + added_html: "Átállítás %{value}-ra/-re" + removed: "Eltávolított " + updated: "Átállítva %{old_value}-ról/-ről %{value}-ra/-re" + updated_html: "Átállítva %{old_value}-ról/-ről %{value}-ra/-re" position: - updated: "reordered" + updated: "Átrakva" work_package: - updated: "changed from %{old_value} to %{value}" - updated_html: "changed from %{old_value} to %{value}" + updated: "Átállítva %{old_value}-ról/-ről %{value}-ra/-re" + updated_html: "Átállítva %{old_value}-ról/-ről %{value}-ra/-re" description_attended: "részt vett" description_invite: "meghívott" events: diff --git a/modules/meeting/config/routes.rb b/modules/meeting/config/routes.rb index 0044abb9ec46..9a23450880a3 100644 --- a/modules/meeting/config/routes.rb +++ b/modules/meeting/config/routes.rb @@ -28,7 +28,11 @@ Rails.application.routes.draw do resources :projects, only: %i[] do - resources :meetings, only: %i[index new create show] + resources :meetings, only: %i[index new create show] do + collection do + get "menu" => "meetings/menus#show" + end + end end resources :work_packages, only: %i[] do @@ -47,6 +51,10 @@ end end + namespace :meetings do + resource :menu, only: %[show] + end + resources :meetings do member do get :cancel_edit diff --git a/modules/meeting/lib/open_project/meeting/engine.rb b/modules/meeting/lib/open_project/meeting/engine.rb index d2a26c53b74c..0669e0933608 100644 --- a/modules/meeting/lib/open_project/meeting/engine.rb +++ b/modules/meeting/lib/open_project/meeting/engine.rb @@ -43,10 +43,12 @@ class Engine < ::Rails::Engine { meetings: %i[index show download_ics participants_dialog history], meeting_agendas: %i[history show diff], meeting_minutes: %i[history show diff], + "meetings/menus": %i[show], work_package_meetings_tab: %i[index count] }, permissible_on: :project permission :create_meetings, - { meetings: %i[new create copy] }, + { meetings: %i[new create copy], + "meetings/menus": %i[show] }, permissible_on: :project, require: :member, contract_actions: { meetings: %i[create] } @@ -120,7 +122,7 @@ class Engine < ::Rails::Engine menu :project_menu, :meetings_query_select, { controller: "/meetings", action: "index" }, parent: :meetings, - partial: "meetings/menu_query_select" + partial: "meetings/menus/menu" should_render_global_menu_item = Proc.new do (User.current.logged? || !Setting.login_required?) && @@ -145,7 +147,7 @@ class Engine < ::Rails::Engine menu :global_menu, :meetings_query_select, { controller: "/meetings", action: "index", project_id: nil }, parent: :meetings, - partial: "meetings/menu_query_select", + partial: "meetings/menus/menu", if: should_render_global_menu_item ActiveSupport::Inflector.inflections do |inflect| diff --git a/modules/meeting/spec/controllers/meetings_controller_spec.rb b/modules/meeting/spec/controllers/meetings_controller_spec.rb index 07f605110ea2..c4543bf4a062 100644 --- a/modules/meeting/spec/controllers/meetings_controller_spec.rb +++ b/modules/meeting/spec/controllers/meetings_controller_spec.rb @@ -29,19 +29,11 @@ require "#{File.dirname(__FILE__)}/../spec_helper" RSpec.describe MeetingsController do - let(:user) { create(:admin) } - let(:project) { create(:project) } - let(:other_project) { create(:project) } + shared_let(:user) { create(:admin) } + shared_let(:project) { create(:project) } + shared_let(:other_project) { create(:project) } - before do - allow(User).to receive(:current).and_return user - - allow(Project).to receive(:find).and_return(project) - - allow(controller).to receive(:authorize) - allow(controller).to receive(:authorize_global) - allow(controller).to receive(:check_if_login_required) - end + current_user { user } describe "GET" do describe "index" do @@ -161,8 +153,6 @@ let(:meeting_params) { base_meeting_params } before do - allow(Project).to receive(:find).and_return(project) - post :create, params: end diff --git a/modules/meeting/spec/features/meetings_global_menu_item_spec.rb b/modules/meeting/spec/features/meetings_global_menu_item_spec.rb index c489fb98320a..09a36150a64d 100644 --- a/modules/meeting/spec/features/meetings_global_menu_item_spec.rb +++ b/modules/meeting/spec/features/meetings_global_menu_item_spec.rb @@ -33,9 +33,11 @@ require_relative "../support/pages/meetings/index" RSpec.describe "Meetings global menu item", + :js, :with_cuprite do shared_let(:user_without_permissions) { create(:user) } shared_let(:admin) { create(:admin) } + shared_let(:project) { create(:project) } shared_let(:meetings_label) { I18n.t(:label_meeting_plural) } let(:meetings_page) { Pages::Meetings::Index.new(project: nil) } diff --git a/modules/meeting/spec/features/meetings_index_spec.rb b/modules/meeting/spec/features/meetings_index_spec.rb index e6ad768f830e..2cc760658268 100644 --- a/modules/meeting/spec/features/meetings_index_spec.rb +++ b/modules/meeting/spec/features/meetings_index_spec.rb @@ -30,7 +30,7 @@ require_relative "../support/pages/meetings/index" -RSpec.describe "Meetings", "Index", :with_cuprite do +RSpec.describe "Meetings", "Index", :js, :with_cuprite do # The order the Projects are created in is important. By naming `project` alphanumerically # after `other_project`, we can ensure that subsequent specs that assert sorting is # correct for the right reasons (sorting by Project name and not id) @@ -209,7 +209,7 @@ def invite_to_meeting(meeting) invite_to_meeting(yesterdays_meeting) invite_to_meeting(other_project_meeting) - meetings_page.navigate_by_modules_menu + meetings_page.visit! meetings_page.expect_meetings_listed(meeting, other_project_meeting) meetings_page.expect_meetings_not_listed(yesterdays_meeting) end @@ -234,7 +234,7 @@ def invite_to_meeting(meeting) let(:permissions) { %i(view_meetings create_meetings) } it "shows the create new buttons" do - meetings_page.navigate_by_modules_menu + meetings_page.visit! meetings_page.expect_create_new_buttons end @@ -244,7 +244,7 @@ def invite_to_meeting(meeting) let(:permissions) { %i[view_meetings] } it "doesn't show a create new button" do - meetings_page.navigate_by_modules_menu + meetings_page.visit! meetings_page.expect_no_create_new_buttons end diff --git a/modules/meeting/spec/features/structured_meetings/work_package_meetings_tab_spec.rb b/modules/meeting/spec/features/structured_meetings/work_package_meetings_tab_spec.rb index c3367a974d8f..861a3c83e07f 100644 --- a/modules/meeting/spec/features/structured_meetings/work_package_meetings_tab_spec.rb +++ b/modules/meeting/spec/features/structured_meetings/work_package_meetings_tab_spec.rb @@ -381,9 +381,11 @@ meetings_tab.open_add_to_meeting_dialog - click_on("Save") + retry_block do + click_on("Save") - expect(page).to have_content("Meeting can't be blank") + expect(page).to have_content("Meeting can't be blank") + end end it "adds presenter when the work package is added to a meeting" do diff --git a/modules/meeting/spec/requests/api/v3/meetings/meetings_resource_spec.rb b/modules/meeting/spec/requests/api/v3/meetings/meetings_resource_spec.rb index ce5637d5989f..094095c8939d 100644 --- a/modules/meeting/spec/requests/api/v3/meetings/meetings_resource_spec.rb +++ b/modules/meeting/spec/requests/api/v3/meetings/meetings_resource_spec.rb @@ -54,7 +54,7 @@ context "when valid id" do it "returns HTTP 200" do - expect(last_response.status).to eq 200 + expect(last_response).to have_http_status :ok end end @@ -62,16 +62,14 @@ let(:permissions) { [:view_work_packages] } it "returns HTTP 404" do - expect(last_response.status).to eq 404 + expect(last_response).to have_http_status :not_found end end context "when invalid id" do let(:get_path) { api_v3_paths.budget "bogus" } - it_behaves_like "param validation error" do - let(:id) { "bogus" } - end + it_behaves_like "not found" end end diff --git a/modules/meeting/spec/services/meetings/copy_service_integration_spec.rb b/modules/meeting/spec/services/meetings/copy_service_integration_spec.rb index 3b6a3addbadf..c103397bc3e0 100644 --- a/modules/meeting/spec/services/meetings/copy_service_integration_spec.rb +++ b/modules/meeting/spec/services/meetings/copy_service_integration_spec.rb @@ -48,7 +48,7 @@ expect(copy.start_time).to eq(meeting.start_time + 1.week) end - context 'when the meeting is closed' do + context "when the meeting is closed" do it "reopens the meeting" do meeting.update! state: "closed" expect(service_result).to be_success diff --git a/modules/meeting/spec/support/pages/meetings/index.rb b/modules/meeting/spec/support/pages/meetings/index.rb index 6c810b3797c5..79384ee10bfb 100644 --- a/modules/meeting/spec/support/pages/meetings/index.rb +++ b/modules/meeting/spec/support/pages/meetings/index.rb @@ -56,7 +56,7 @@ def expect_no_create_new_buttons expect(page).not_to have_test_selector("add-meeting-button") within "#main-menu" do - expect(page).to have_no_button "Meeting" + expect(page).not_to have_test_selector "meeting--create-button" end end @@ -68,7 +68,7 @@ def expect_create_new_buttons expect(page).to have_test_selector("add-meeting-button") within "#main-menu" do - expect(page).to have_button "Meeting" + expect(page).to have_test_selector "meeting--create-button" end end diff --git a/modules/my_page/lib/my_page/grid_registration.rb b/modules/my_page/lib/my_page/grid_registration.rb index fca2a6304586..665f6e1bfb8b 100644 --- a/modules/my_page/lib/my_page/grid_registration.rb +++ b/modules/my_page/lib/my_page/grid_registration.rb @@ -23,11 +23,19 @@ class GridRegistration < ::Grids::Configuration::Registration options_representer "::API::V3::Grids::Widgets::QueryOptionsRepresenter" end + # Allow users without save_queries permission to access the widgets + # but they are not allowed to update the underlying query + wp_static_table_strategy_proc = Proc.new do + after_destroy -> { ::Query.find_by(id: options[:queryId])&.destroy } + + options_representer "::API::V3::Grids::Widgets::QueryOptionsRepresenter" + end + widget_strategy "work_packages_table", &wp_table_strategy_proc - widget_strategy "work_packages_assigned", &wp_table_strategy_proc - widget_strategy "work_packages_accountable", &wp_table_strategy_proc - widget_strategy "work_packages_watched", &wp_table_strategy_proc - widget_strategy "work_packages_created", &wp_table_strategy_proc + widget_strategy "work_packages_assigned", &wp_static_table_strategy_proc + widget_strategy "work_packages_accountable", &wp_static_table_strategy_proc + widget_strategy "work_packages_watched", &wp_static_table_strategy_proc + widget_strategy "work_packages_created", &wp_static_table_strategy_proc widget_strategy "time_entries_current_user" do options_representer "::API::V3::Grids::Widgets::TimeEntryCalendarOptionsRepresenter" diff --git a/modules/my_page/spec/features/my/work_package_watcher_widget_spec.rb b/modules/my_page/spec/features/my/work_package_watcher_widget_spec.rb new file mode 100644 index 000000000000..16dfbfde4ac5 --- /dev/null +++ b/modules/my_page/spec/features/my/work_package_watcher_widget_spec.rb @@ -0,0 +1,61 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" + +require_relative "../../support/pages/my/page" + +RSpec.describe "Work package watched widget on My page", :js do + shared_let(:user) { create(:user) } + shared_let(:non_member) { create(:non_member, permissions: [:view_work_packages]) } + shared_let(:project) { create(:project, public: true) } + shared_let(:work_package) do + create(:work_package, + project:, + subject: "Visible work package for non member", + author: user, + responsible: user) + end + + let(:my_page) do + Pages::My::Page.new + end + + before do + login_as user + work_package.add_watcher(user) + + my_page.visit! + end + + it "can add the watcher widget without being member anywhere (Regression #55838)" do + my_page.add_widget(1, 1, :within, "Work packages watched by me") + + expect(page).to have_text(work_package.subject) + end +end diff --git a/modules/overviews/lib/overviews/engine.rb b/modules/overviews/lib/overviews/engine.rb index df7946956caf..9e6ac53cf389 100644 --- a/modules/overviews/lib/overviews/engine.rb +++ b/modules/overviews/lib/overviews/engine.rb @@ -60,7 +60,10 @@ class Engine < ::Rails::Engine OpenProject::AccessControl.permission(:view_work_packages) .controller_actions - .push("overviews/overviews/show") + .push( + "overviews/overviews/show", + "overviews/overviews/project_custom_fields_sidebar" + ) OpenProject::AccessControl.map do |ac_map| ac_map.project_module nil do |map| diff --git a/modules/recaptcha/spec/controllers/admin_controller_spec.rb b/modules/recaptcha/spec/controllers/admin_controller_spec.rb index b621683c2e76..881057e8945a 100644 --- a/modules/recaptcha/spec/controllers/admin_controller_spec.rb +++ b/modules/recaptcha/spec/controllers/admin_controller_spec.rb @@ -12,10 +12,10 @@ it "does not allow access" do get :show - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden post :update - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end end diff --git a/modules/reporting/app/controllers/cost_reports/menus_controller.rb b/modules/reporting/app/controllers/cost_reports/menus_controller.rb new file mode 100644 index 000000000000..a886c94f05e8 --- /dev/null +++ b/modules/reporting/app/controllers/cost_reports/menus_controller.rb @@ -0,0 +1,38 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-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 ::CostReports + class MenusController < ApplicationController + before_action :load_and_authorize_in_optional_project + + def show + @submenu_menu_items = ::CostReports::Menu.new(project: @project, params:).menu_items + + render layout: nil + end + end +end diff --git a/modules/reporting/app/controllers/cost_reports_controller.rb b/modules/reporting/app/controllers/cost_reports_controller.rb index 81880f56d245..4d074fc19d37 100644 --- a/modules/reporting/app/controllers/cost_reports_controller.rb +++ b/modules/reporting/app/controllers/cost_reports_controller.rb @@ -109,7 +109,7 @@ def table end def menu_item_to_highlight_on_index - @project ? :costs : :cost_reports_global_report_menu + @project ? :costs : :cost_reports_global end ## diff --git a/modules/reporting/app/menus/cost_reports/menu.rb b/modules/reporting/app/menus/cost_reports/menu.rb new file mode 100644 index 000000000000..fabd29c03f6e --- /dev/null +++ b/modules/reporting/app/menus/cost_reports/menu.rb @@ -0,0 +1,74 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-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 CostReports + class Menu < Submenu + attr_reader :view_type, :project + + def initialize(project: nil, params: nil) + @project = project + @params = params + + super(view_type:, project:, params:) + end + + def menu_items + [ + OpenProject::Menu::MenuGroup.new(header: I18n.t("label_public_report_plural"), children: global_queries), + OpenProject::Menu::MenuGroup.new(header: I18n.t("label_private_report_plural"), children: custom_queries) + ] + end + + def global_queries + CostQuery.public(project) + .pluck(:id, :name) + .map { |id, name| menu_item(name, query_params(id)) } + end + + def custom_queries + CostQuery.private(project, User.current) + .pluck(:id, :name) + .map { |id, name| menu_item(name, query_params(id)) } + end + + def selected?(query_params) + query_params[:id].to_s == params[:id] + end + + def query_params(id) + { id: } + end + + def query_path(query_params) + if project.present? + project_cost_report_path(project, query_params) + else + global_cost_report_path(query_params) + end + end + end +end diff --git a/modules/reporting/app/views/cost_reports/menus/_menu.html.erb b/modules/reporting/app/views/cost_reports/menus/_menu.html.erb new file mode 100644 index 000000000000..82625223b248 --- /dev/null +++ b/modules/reporting/app/views/cost_reports/menus/_menu.html.erb @@ -0,0 +1,5 @@ +<%= turbo_frame_tag "cost_reports_sidemenu", + src: @project ? menu_project_cost_reports_path(@project, **params.permit(:id)) : cost_reports_menu_path(**params.permit(:id)), + target: "_top", + data: { turbo: false }, + loading: :lazy %> diff --git a/modules/reporting/app/views/cost_reports/menus/show.html.erb b/modules/reporting/app/views/cost_reports/menus/show.html.erb new file mode 100644 index 000000000000..d3f4ad6bc7ae --- /dev/null +++ b/modules/reporting/app/views/cost_reports/menus/show.html.erb @@ -0,0 +1,3 @@ +<%= turbo_frame_tag "cost_reports_sidemenu" do %> + <%= render OpenProject::Common::SubmenuComponent.new(sidebar_menu_items: @submenu_menu_items) %> +<% end %> diff --git a/modules/reporting/config/locales/crowdin/zh-TW.yml b/modules/reporting/config/locales/crowdin/zh-TW.yml index 49d48a9ab8fd..275d46a22940 100644 --- a/modules/reporting/config/locales/crowdin/zh-TW.yml +++ b/modules/reporting/config/locales/crowdin/zh-TW.yml @@ -47,7 +47,7 @@ zh-TW: label_is_project_with_subprojects: "是 (包括子專案)" label_work_package_attributes: "工作包屬性" label_less: "<" - label_logged_by_reporting: "所記錄" + label_logged_by_reporting: "記錄者" label_money: "現金價值" label_month_reporting: "月" label_new_report: "新建成本報表" diff --git a/modules/reporting/config/routes.rb b/modules/reporting/config/routes.rb index 1cc8ea7b225e..fdab1c5d5fd7 100644 --- a/modules/reporting/config/routes.rb +++ b/modules/reporting/config/routes.rb @@ -31,15 +31,21 @@ resources :cost_reports, except: :create do collection do match :index, via: %i[get post] + get "menu" => "cost_reports/menus#show", as: :menu_project end member do + get :index, as: :project post :update post :rename end end end + namespace :cost_reports do + resource :menu, only: %[show] + end + resources :cost_reports, except: :create do collection do match :index, via: %i[get post] @@ -49,6 +55,7 @@ end member do + get :index, as: :global post :update post :rename end diff --git a/modules/reporting/lib/open_project/reporting/engine.rb b/modules/reporting/lib/open_project/reporting/engine.rb index fca3ae6f8bac..c8d62b9f1eb7 100644 --- a/modules/reporting/lib/open_project/reporting/engine.rb +++ b/modules/reporting/lib/open_project/reporting/engine.rb @@ -57,6 +57,11 @@ class Engine < ::Rails::Engine OpenProject::AccessControl.permission(:view_cost_entries).controller_actions << "cost_reports/#{action}" OpenProject::AccessControl.permission(:view_own_cost_entries).controller_actions << "cost_reports/#{action}" end + + OpenProject::AccessControl.permission(:view_time_entries).controller_actions << "cost_reports/menus/show" + OpenProject::AccessControl.permission(:view_own_time_entries).controller_actions << "cost_reports/menus/show" + OpenProject::AccessControl.permission(:view_cost_entries).controller_actions << "cost_reports/menus/show" + OpenProject::AccessControl.permission(:view_own_cost_entries).controller_actions << "cost_reports/menus/show" end end @@ -90,7 +95,7 @@ class Engine < ::Rails::Engine :cost_reports_global_report_menu, { controller: "/cost_reports", action: "index", project_id: nil }, parent: :cost_reports_global, - partial: "cost_reports/report_menu", + partial: "cost_reports/menus/menu", if: should_render menu :project_menu, @@ -105,7 +110,7 @@ class Engine < ::Rails::Engine :costs_menu, { controller: "/cost_reports", action: "index" }, if: Proc.new { |project| project.module_enabled?(:costs) }, - partial: "/cost_reports/report_menu", + partial: "cost_reports/menus/menu", parent: :costs end diff --git a/modules/reporting/lib/widget/cost_types.rb b/modules/reporting/lib/widget/cost_types.rb index 67e63d3c7769..9ac7b4aac47c 100644 --- a/modules/reporting/lib/widget/cost_types.rb +++ b/modules/reporting/lib/widget/cost_types.rb @@ -31,7 +31,7 @@ def render_with_options(options, &) @cost_types = options.delete(:cost_types) @selected_type_id = options.delete(:selected_type_id) - super(options, &) + super end def render diff --git a/modules/reporting/lib/widget/reporting_widget.rb b/modules/reporting/lib/widget/reporting_widget.rb index e413e5ffe088..3e198fac1a70 100644 --- a/modules/reporting/lib/widget/reporting_widget.rb +++ b/modules/reporting/lib/widget/reporting_widget.rb @@ -40,7 +40,7 @@ class Widget::ReportingWidget < ActionView::Base attr_accessor :output_buffer, :controller, :config, :_content_for, :_routes, :subject def self.new(subject) - super(subject).tap do |o| + super.tap do |o| o.subject = subject end end diff --git a/modules/reporting/spec/features/top_menu_item_spec.rb b/modules/reporting/spec/features/top_menu_item_spec.rb index 137fcc62e31c..b368fccf03cf 100644 --- a/modules/reporting/spec/features/top_menu_item_spec.rb +++ b/modules/reporting/spec/features/top_menu_item_spec.rb @@ -29,6 +29,8 @@ require "spec_helper" RSpec.describe "Top menu items", :js do + shared_let(:project) { create(:project) } + let(:user) { create(:user) } let(:open_menu) { true } diff --git a/modules/reporting/spec/workers/cost_query/export_job_spec.rb b/modules/reporting/spec/workers/cost_query/export_job_spec.rb index c54892d0b432..870212529413 100644 --- a/modules/reporting/spec/workers/cost_query/export_job_spec.rb +++ b/modules/reporting/spec/workers/cost_query/export_job_spec.rb @@ -31,7 +31,7 @@ require "spec_helper" RSpec.describe CostQuery::ExportJob do - let(:user) { build_stubbed(:admin) } + let(:user) { build_stubbed(:user) } let(:project) { build_stubbed(:project) } let(:initial_filter_params) do @@ -46,6 +46,10 @@ } end + before do + mock_permissions_for(user, &:allow_everything) + end + # Performs a cost export with the given extra filters. # # @param extra_filters [Hash] A hash of attribute names and operator/value diff --git a/modules/storages/app/common/storages/errors.rb b/modules/storages/app/common/storages/errors.rb index 00d5fb824153..0150d948d256 100644 --- a/modules/storages/app/common/storages/errors.rb +++ b/modules/storages/app/common/storages/errors.rb @@ -48,25 +48,17 @@ def message end end - class IntegrationJobError < BaseError - attr_reader :errors, :storage - - def initialize(storage:, errors:) - super(errors.log_message) - @storage = storage - @errors = errors - end - end + class IntegrationJobError < BaseError; end def self.registry_error_for(key) - case key.split('.') - in [storage, 'contracts', model] + case key.split(".") + in [storage, "contracts", model] MissingContract.new("No #{model} contract defined for provider: #{storage.camelize}") - in [storage, 'commands' | 'queries' => type, operation] + in [storage, "commands" | "queries" => type, operation] OperationNotSupported.new( "#{type.singularize.capitalize} #{operation} not supported by provider: #{storage.camelize}" ) - in [storage, 'models', object] + in [storage, "models", object] MissingModel.new("Model #{object} not registered for provider: #{storage.camelize}") else ResolverStandardError.new("Cannot resolve key #{key}.") diff --git a/modules/storages/app/common/storages/peripherals/one_drive_connection_validator.rb b/modules/storages/app/common/storages/peripherals/one_drive_connection_validator.rb new file mode 100644 index 000000000000..0d53bc9cb960 --- /dev/null +++ b/modules/storages/app/common/storages/peripherals/one_drive_connection_validator.rb @@ -0,0 +1,162 @@ +# frozen_string_literal:true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Storages + module Peripherals + class OneDriveConnectionValidator + include Dry::Monads[:maybe] + + using ServiceResultRefinements + + def initialize(storage:) + @storage = storage + end + + def validate + maybe_is_not_configured + .or { tenant_id_wrong } + .or { client_id_wrong } + .or { client_secret_wrong } + .or { drive_id_wrong } + .or { request_failed_with_unknown_error } + .or { drive_with_unexpected_content } + .value_or(ConnectionValidation.new(type: :healthy, + error_code: :none, + timestamp: Time.current, + description: nil)) + end + + private + + def query + @query ||= Peripherals::Registry + .resolve("#{@storage.short_provider_type}.queries.files") + .call(storage: @storage, auth_strategy:, folder: root_folder) + end + + def maybe_is_not_configured + return None() if @storage.configured? + + Some(ConnectionValidation.new(type: :none, + error_code: :wrn_not_configured, + timestamp: Time.current, + description: I18n.t("storages.health.connection_validation.not_configured"))) + end + + def drive_id_wrong + return None() if query.result != :not_found + + Some(ConnectionValidation.new(type: :error, + error_code: :err_drive_invalid, + timestamp: Time.current, + description: I18n.t("storages.health.connection_validation.drive_id_wrong"))) + end + + def tenant_id_wrong + return None() if query.result != :unauthorized + + payload = JSON.parse(query.error_payload) + return None() if payload["error"] != "invalid_request" + + tenant_id_string = "Tenant '#{@storage.tenant_id}' not found." + return None() unless payload["error_description"].include?(tenant_id_string) + + Some(ConnectionValidation.new(type: :error, + error_code: :err_tenant_invalid, + timestamp: Time.current, + description: I18n.t("storages.health.connection_validation.tenant_id_wrong"))) + end + + def client_id_wrong + return None() if query.result != :unauthorized + + payload = JSON.parse(query.error_payload) + return None() if payload["error"] != "unauthorized_client" + + Some(ConnectionValidation.new(type: :error, + error_code: :err_client_invalid, + timestamp: Time.current, + description: I18n.t("storages.health.connection_validation.client_id_wrong"))) + end + + def client_secret_wrong + return None() if query.result != :unauthorized + + payload = JSON.parse(query.error_payload) + return None() if payload["error"] != "invalid_client" + + Some(ConnectionValidation.new(type: :error, + error_code: :err_client_invalid, + timestamp: Time.current, + description: I18n.t("storages.health.connection_validation.client_secret_wrong"))) + end + + # rubocop:disable Metrics/AbcSize + def drive_with_unexpected_content + return None() if query.failure? + return None() unless @storage.automatic_management_enabled? + + expected_folder_ids = @storage.project_storages + .where(project_folder_mode: "automatic") + .map(&:project_folder_id) + + unexpected_files = query.result.files.reject { |file| expected_folder_ids.include?(file.id) } + return None() if unexpected_files.empty? + + Some(ConnectionValidation.new(type: :warning, + error_code: :wrn_unexpected_content, + timestamp: Time.current, + description: I18n.t("storages.health.connection_validation.unexpected_content"))) + end + + # rubocop:enable Metrics/AbcSize + + def request_failed_with_unknown_error + return None() if query.success? + + Rails.logger.error("Connection validation failed with unknown error:\n\t" \ + "status: #{query.result}\n\tresponse: #{query.error_payload}") + + Some(ConnectionValidation.new(type: :error, + error_code: :err_unknown, + timestamp: Time.current, + description: I18n.t("storages.health.connection_validation.unknown_error"))) + end + + def root_folder + Peripherals::ParentFolder.new("/") + end + + def auth_strategy + Peripherals::Registry.resolve("#{@storage.short_provider_type}.authentication.userless").call + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/rename_file_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/rename_file_command.rb index 12f4da101eec..b30ae1537f40 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/rename_file_command.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/rename_file_command.rb @@ -28,47 +28,82 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Storages::Peripherals::StorageInteraction::Nextcloud - class RenameFileCommand - using Storages::Peripherals::ServiceResultRefinements +module Storages + module Peripherals + module StorageInteraction + module Nextcloud + class RenameFileCommand + def self.call(storage:, auth_strategy:, file_id:, name:) + new(storage).call(auth_strategy:, file_id:, name:) + end - def initialize(storage) - @uri = storage.uri - @base_path = Util.join_uri_path(@uri, "remote.php/dav/files", CGI.escapeURIComponent(storage.username)) - @username = storage.username - @password = storage.password - end + def initialize(storage) + @storage = storage + end - def self.call(storage:, source:, target:) - new(storage).call(source:, target:) - end + def call(auth_strategy:, file_id:, name:) + validate_input_data(file_id:, name:).on_failure { |failure| return failure } + + origin_user_id = Util.origin_user_id(caller: self.class, storage: @storage, auth_strategy:) + .on_failure { |failure| return failure } + .result + + info = FileInfoQuery.call(storage: @storage, auth_strategy:, file_id:) + .on_failure { |failure| return failure } + .result + + make_request(auth_strategy, origin_user_id, info, name).on_failure { |failure| return failure } + + FileInfoQuery.call(storage: @storage, auth_strategy:, file_id:) + .map { |file_info| Util.storage_file_from_file_info(file_info) } + end + + private + + def make_request(auth_strategy, user, file_info, name) + source_path = Util.join_uri_path(@storage.uri, + "remote.php/dav/files", + user, + file_info.location) + destination = Util.join_uri_path(@storage.uri.path, + "remote.php/dav/files", + user, + target_path(file_info, name)) + + Authentication[auth_strategy].call(storage: @storage) do |http| + handle_response http.request("MOVE", source_path, headers: { "Destination" => destination }) + end + end - def call(source:, target:) - response = OpenProject - .httpx - .basic_auth(@username, @password) - .request( - "MOVE", - Util.join_uri_path(@base_path, Util.escape_path(source)), - headers: { - "Destination" => Util.join_uri_path(@uri.path, - "remote.php/dav/files", - CGI.escapeURIComponent(@username), - Util.escape_path(target)) - } - ) + def target_path(info, name) + info.location.gsub(CGI.escapeURIComponent(info.name), CGI.escapeURIComponent(name)) + end - error_data = Storages::StorageErrorData.new(source: self.class, payload: response) + def validate_input_data(file_id:, name:) + if file_id.blank? || name.blank? + ServiceResult.failure(result: :error, + errors: StorageError.new(code: :error, + data: StorageErrorData.new(source: self.class), + log_message: "Invalid input data!")) + else + ServiceResult.success + end + end - case response - in { status: 200..299 } - ServiceResult.success - in { status: 404 } - Util.error(:not_found, "Outbound request destination not found", error_data) - in { status: 401 } - Util.error(:unauthorized, "Outbound request not authorized", error_data) - else - Util.error(:error, "Outbound request failed", error_data) + def handle_response(response) + error_data = StorageErrorData.new(source: self.class, payload: response) + case response + in { status: 200..299 } + ServiceResult.success + in { status: 404 } + Util.error(:not_found, "Outbound request destination not found", error_data) + in { status: 401 } + Util.error(:unauthorized, "Outbound request not authorized", error_data) + else + Util.error(:error, "Outbound request failed", error_data) + end + end + end end end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/util.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/util.rb index 23ebcad28f5b..5e88a3acc360 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/util.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/util.rb @@ -134,6 +134,21 @@ def error_data_from_response(caller:, response:) def failure(code:, data:, log_message:) ServiceResult.failure(result: code, errors: StorageError.new(code:, data:, log_message:)) end + + def storage_file_from_file_info(storage_file_info) + 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 end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/auth_check_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/auth_check_query.rb index efb9588f8ae9..9fe43ff52b4f 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/auth_check_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/auth_check_query.rb @@ -33,10 +33,6 @@ module Peripherals module StorageInteraction module OneDrive class AuthCheckQuery - Auth = ::Storages::Peripherals::StorageInteraction::Authentication - - using ServiceResultRefinements - def self.call(storage:, auth_strategy:) new(storage).call(auth_strategy:) end @@ -46,7 +42,7 @@ def initialize(storage) end def call(auth_strategy:) - Auth[auth_strategy].call(storage: @storage) do |http| + Authentication[auth_strategy].call(storage: @storage) do |http| handle_response http.get(Util.join_uri_path(@storage.uri, "/v1.0/me")) end end @@ -58,12 +54,12 @@ def handle_response(response) in { status: 200..299 } ServiceResult.success in { status: 401 } - ServiceResult.failure(result: :unauthorized, errors: ::Storages::StorageError.new(code: :unauthorized)) + ServiceResult.failure(result: :unauthorized, errors: StorageError.new(code: :unauthorized)) in { status: 403 } - ServiceResult.failure(result: :forbidden, errors: ::Storages::StorageError.new(code: :forbidden)) + ServiceResult.failure(result: :forbidden, errors: StorageError.new(code: :forbidden)) else - data = ::Storages::StorageErrorData.new(source: self.class, payload: response) - ServiceResult.failure(result: :error, errors: ::Storages::StorageError.new(code: :error, data:)) + data = StorageErrorData.new(source: self.class, payload: response) + ServiceResult.failure(result: :error, errors: StorageError.new(code: :error, data:)) end end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/create_folder_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/create_folder_command.rb index 3ea823670129..e7c5db323542 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/create_folder_command.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/create_folder_command.rb @@ -64,7 +64,7 @@ def uri_for(parent_location) def handle_response(response) case response in { status: 200..299 } - ServiceResult.success(result: file_info_for(MultiJson.load(response.body, symbolize_keys: true)), + ServiceResult.success(result: Util.storage_file_from_json(MultiJson.load(response.body, symbolize_keys: true)), message: "Folder was successfully created.") in { status: 404 } ServiceResult.failure(result: :not_found, @@ -81,21 +81,6 @@ def handle_response(response) end end - def file_info_for(json_file) - StorageFile.new( - id: json_file[:id], - name: json_file[:name], - size: json_file[:size], - mime_type: Util.mime_type(json_file), - created_at: Time.zone.parse(json_file.dig(:fileSystemInfo, :createdDateTime)), - last_modified_at: Time.zone.parse(json_file.dig(:fileSystemInfo, :lastModifiedDateTime)), - created_by_name: json_file.dig(:createdBy, :user, :displayName), - last_modified_by_name: json_file.dig(:lastModifiedBy, :user, :displayName), - location: Util.extract_location(json_file[:parentReference], json_file[:name]), - permissions: %i[readable writeable] - ) - end - def payload(folder_name) { name: folder_name, diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/download_link_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/download_link_query.rb index 6c1e50c3a266..000ac6c43e1c 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/download_link_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/download_link_query.rb @@ -33,8 +33,6 @@ module Peripherals module StorageInteraction module OneDrive class DownloadLinkQuery - Auth = ::Storages::Peripherals::StorageInteraction::Authentication - def self.call(storage:, auth_strategy:, file_link:) new(storage).call(auth_strategy:, file_link:) end @@ -50,7 +48,7 @@ def call(auth_strategy:, file_link:) log_message: "File link can not be nil.")) end - Auth[auth_strategy].call(storage: @storage) do |http| + Authentication[auth_strategy].call(storage: @storage) do |http| handle_errors http.get(Util.join_uri_path(@storage.uri, uri_path_for(file_link.origin_id))) end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_query.rb index f9d90395bca7..4c0d64d7cca4 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_query.rb @@ -35,8 +35,6 @@ module OneDrive class FilesQuery FIELDS = "?$select=id,name,size,webUrl,lastModifiedBy,createdBy,fileSystemInfo,file,folder,parentReference" - using ServiceResultRefinements - def self.call(storage:, auth_strategy:, folder:) new(storage).call(auth_strategy:, folder:) end @@ -81,27 +79,12 @@ def handle_response(response, map_value) end def storage_files(json_files) - files = json_files.map { |json| storage_file(json) } + files = json_files.map { |json| Util.storage_file_from_json(json) } parent_reference = json_files.first[:parentReference] StorageFiles.new(files, parent(parent_reference), forge_ancestors(parent_reference)) end - def storage_file(json_file) - StorageFile.new( - id: json_file[:id], - name: json_file[:name], - size: json_file[:size], - mime_type: Util.mime_type(json_file), - created_at: Time.zone.parse(json_file.dig(:fileSystemInfo, :createdDateTime)), - last_modified_at: Time.zone.parse(json_file.dig(:fileSystemInfo, :lastModifiedDateTime)), - created_by_name: json_file.dig(:createdBy, :user, :displayName), - last_modified_by_name: json_file.dig(:lastModifiedBy, :user, :displayName), - location: Util.escape_path(Util.extract_location(json_file[:parentReference], json_file[:name])), - permissions: %i[readable writeable] - ) - end - def empty_response(http, folder) response = http.get(Util.join_uri_path(@uri, location_uri_path_for(folder) + FIELDS)) handle_response(response, :id).map do |parent_location_id| diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/open_file_link_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/open_file_link_query.rb index 8409576c9aca..9f548d11fb94 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/open_file_link_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/open_file_link_query.rb @@ -33,9 +33,6 @@ module Peripherals module StorageInteraction module OneDrive class OpenFileLinkQuery - using ::Storages::Peripherals::ServiceResultRefinements - Auth = ::Storages::Peripherals::StorageInteraction::Authentication - def self.call(storage:, auth_strategy:, file_id:, open_location: false) new(storage).call(auth_strategy:, file_id:, open_location:) end @@ -46,7 +43,7 @@ def initialize(storage) end def call(auth_strategy:, file_id:, open_location: false) - Auth[auth_strategy].call(storage: @storage) do |http| + Authentication[auth_strategy].call(storage: @storage) do |http| if open_location request_parent_id(http, file_id).on_success { |parent_id| return request_web_url(http, parent_id.result) } else diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/open_storage_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/open_storage_query.rb index 8a7242e1da2b..33ea6da2d933 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/open_storage_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/open_storage_query.rb @@ -33,8 +33,6 @@ module Peripherals module StorageInteraction module OneDrive class OpenStorageQuery - Auth = ::Storages::Peripherals::StorageInteraction::Authentication - def self.call(storage:, auth_strategy:) new(storage).call(auth_strategy:) end @@ -44,7 +42,7 @@ def initialize(storage) end def call(auth_strategy:) - Auth[auth_strategy].call(storage: @storage) do |http| + Authentication[auth_strategy].call(storage: @storage) do |http| request_drive(http).map(&web_url) end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/rename_file_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/rename_file_command.rb index 5099b2b72519..e236ec5c9048 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/rename_file_command.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/rename_file_command.rb @@ -33,29 +33,44 @@ module Peripherals module StorageInteraction module OneDrive class RenameFileCommand - def self.call(storage:, source:, target:) - new(storage).call(source:, target:) + def self.call(storage:, auth_strategy:, file_id:, name:) + new(storage).call(auth_strategy:, file_id:, name:) end def initialize(storage) @storage = storage - @uri = storage.uri end - def call(source:, target:) - Util.using_admin_token(@storage) do |http| - response = http.patch(uri_path(source), body: { name: target }.to_json) + def call(auth_strategy:, file_id:, name:) + validate_input_data(file_id:, name:).on_failure { |failure| return failure } - handle_response(response) + Authentication[auth_strategy].call(storage: @storage, http_options: Util.json_content_type) do |http| + handle_response http.patch(uri_path(file_id), body: { name: }.to_json) end end private + def validate_input_data(file_id:, name:) + if file_id.blank? || name.blank? + ServiceResult.failure(result: :error, + errors: StorageError.new(code: :error, + data: StorageErrorData.new(source: self.class), + log_message: "Invalid input data!")) + else + ServiceResult.success + end + end + + def uri_path(source) + "#{@storage.uri}v1.0/drives/#{@storage.drive_id}/items/#{source}" + end + + # rubocop:disable Metrics/AbcSize def handle_response(response) case response in { status: 200..299 } - ServiceResult.success(result: storage_file(response.json(symbolize_keys: true))) + ServiceResult.success(result: Util.storage_file_from_json(response.json(symbolize_keys: true))) in { status: 401 } ServiceResult.failure(result: :unauthorized, errors: Util.storage_error(response:, code: :unauthorized, source: self.class)) @@ -74,24 +89,7 @@ def handle_response(response) end end - def storage_file(json_file) - StorageFile.new( - id: json_file[:id], - name: json_file[:name], - size: json_file[:size], - mime_type: Util.mime_type(json_file), - created_at: Time.zone.parse(json_file.dig(:fileSystemInfo, :createdDateTime)), - last_modified_at: Time.zone.parse(json_file.dig(:fileSystemInfo, :lastModifiedDateTime)), - created_by_name: json_file.dig(:createdBy, :user, :displayName), - last_modified_by_name: json_file.dig(:lastModifiedBy, :user, :displayName), - location: Util.extract_location(json_file[:parentReference], json_file[:name]), - permissions: %i[readable writeable] - ) - end - - def uri_path(source) - "/v1.0/drives/#{@storage.drive_id}/items/#{source}" - end + # rubocop:enable Metrics/AbcSize end end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb index f7666b8e78db..059d4891cc5c 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb @@ -125,6 +125,21 @@ def extract_location(parent_reference, file_name = "") appendix = file_name.blank? ? "" : "/#{file_name}" location.empty? ? "/#{file_name}" : "#{location}#{appendix}" end + + def storage_file_from_json(json) + StorageFile.new( + id: json[:id], + name: json[:name], + size: json[:size], + mime_type: Util.mime_type(json), + created_at: Time.zone.parse(json.dig(:fileSystemInfo, :createdDateTime)), + last_modified_at: Time.zone.parse(json.dig(:fileSystemInfo, :lastModifiedDateTime)), + created_by_name: json.dig(:createdBy, :user, :displayName), + last_modified_by_name: json.dig(:lastModifiedBy, :user, :displayName), + location: Util.escape_path(Util.extract_location(json[:parentReference], json[:name])), + permissions: %i[readable writeable] + ) + end end end end diff --git a/modules/storages/app/components/storages/admin/sidebar/health_status_component.html.erb b/modules/storages/app/components/storages/admin/sidebar/health_status_component.html.erb index 013a12725891..81159ee25382 100644 --- a/modules/storages/app/components/storages/admin/sidebar/health_status_component.html.erb +++ b/modules/storages/app/components/storages/admin/sidebar/health_status_component.html.erb @@ -31,34 +31,53 @@ See COPYRIGHT and LICENSE files for more details. component_wrapper do flex_layout do |health_status_container| health_status_container.with_row do - flex_layout do |heading| - heading.with_row do - render(Primer::Beta::Heading.new(tag: :h4)) { I18n.t("storages.health.title") } - end + render(Primer::Beta::Heading.new(tag: :h4)) { I18n.t("storages.health.title") } + end - heading.with_row(mt: 2) do - render(Primer::Beta::Text.new(font_weight: :bold)) { I18n.t("storages.health.subtitle") } + if @storage.provider_type_one_drive? + health_status_container.with_row(mt: 2) do + render(Storages::Admin::Sidebar::ValidationResultComponent.new(result: validation_result_placeholder)) + end + + health_status_container.with_row(mt: 2) do + primer_form_with( + model: @storage, + url: validate_connection_admin_settings_storage_connection_validation_path(@storage), + method: :post, + data: { turbo: true } + ) do + render(Primer::Beta::Button.new( + scheme: :link, + color: :default, + font_weight: :bold, + type: :submit, + )) do |button| + button.with_leading_visual_icon(icon: "meter") + I18n.t("storages.health.connection_validation.action") + end end end end - health_status_container.with_row(mt: 2) do - flex_layout do |health_status_label| - health_status_label.with_row do - concat(render(Primer::Beta::Text.new(pr: 2, test_selector: "storage-health-checked-at")) do - I18n.t('storages.health.checked', datetime: helpers.format_time(@storage.health_checked_at)) - end) + if @storage.automatic_management_enabled? + health_status_container.with_row(mt: 2) do + render(Primer::Beta::Text.new(font_weight: :bold)) { I18n.t("storages.health.project_folders.subtitle") } + end - concat(render(Primer::Beta::Label.new(scheme: health_status_indicator[:scheme], test_selector: "storage-health-status")) do - health_status_indicator[:label] - end) - end + health_status_container.with_row(mt: 2) do + concat(render(Primer::Beta::Text.new(pr: 2, test_selector: "storage-health-checked-at")) do + I18n.t('storages.health.checked', datetime: helpers.format_time(@storage.health_checked_at)) + end) + + concat(render(Primer::Beta::Label.new(scheme: health_status_indicator[:scheme], test_selector: "storage-health-status")) do + health_status_indicator[:label] + end) + end - if @storage.health_unhealthy? - health_status_label.with_row(mt: 2) do - render(Primer::Beta::Text.new(color: :muted, test_selector: "storage-health-error")) do - formatted_health_reason - end + if @storage.health_unhealthy? + health_status_container.with_row(mt: 2) do + render(Primer::Beta::Text.new(color: :muted, test_selector: "storage-health-error")) do + formatted_health_reason end end end diff --git a/modules/storages/app/components/storages/admin/sidebar/health_status_component.rb b/modules/storages/app/components/storages/admin/sidebar/health_status_component.rb index 42e05f958ca9..97a3383375a9 100644 --- a/modules/storages/app/components/storages/admin/sidebar/health_status_component.rb +++ b/modules/storages/app/components/storages/admin/sidebar/health_status_component.rb @@ -27,36 +27,47 @@ # # See COPYRIGHT and LICENSE files for more details. #++ -# -module Storages::Admin - class Sidebar::HealthStatusComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent - include ApplicationHelper - include OpTurbo::Streamable - include OpPrimer::ComponentHelpers - def initialize(storage:) - super(storage) - @storage = storage - end +module Storages + module Admin + module Sidebar + class HealthStatusComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + include ApplicationHelper + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers - private + def initialize(storage:) + super(storage) + @storage = storage + end - def health_status_indicator - case @storage.health_status - when "healthy" - { scheme: :success, label: I18n.t("storages.health.label_healthy") } - when "unhealthy" - { scheme: :danger, label: I18n.t("storages.health.label_error") } - else - { scheme: :attention, label: I18n.t("storages.health.label_pending") } - end - end + private - # This method returns the health identifier, description and the time since when the error occurs in a - # formatted manner. e.g. "Not found: Outbound request destination not found since 12/07/2023 03:45 PM" - def formatted_health_reason - "#{@storage.health_reason_identifier.tr('_', ' ').strip.capitalize}: #{@storage.health_reason_description} " + - I18n.t("storages.health.since", datetime: helpers.format_time(@storage.health_changed_at)) + def health_status_indicator + case @storage.health_status + when "healthy" + { scheme: :success, label: I18n.t("storages.health.label_healthy") } + when "unhealthy" + { scheme: :danger, label: I18n.t("storages.health.label_error") } + else + { scheme: :attention, label: I18n.t("storages.health.label_pending") } + end + end + + # This method returns the health identifier, description and the time since when the error occurs in a + # formatted manner. e.g. "Not found: Outbound request destination not found since 12/07/2023 03:45 PM" + def formatted_health_reason + "#{@storage.health_reason_identifier.tr('_', ' ').strip.capitalize}: #{@storage.health_reason_description} " + + I18n.t("storages.health.since", datetime: helpers.format_time(@storage.health_changed_at)) + end + + def validation_result_placeholder + ConnectionValidation.new(type: :none, + error_code: :none, + timestamp: Time.current, + description: I18n.t("storages.health.connection_validation.placeholder")) + end + end end end end diff --git a/modules/storages/app/components/storages/admin/sidebar/validation_result_component.html.erb b/modules/storages/app/components/storages/admin/sidebar/validation_result_component.html.erb new file mode 100644 index 000000000000..b50ef2442045 --- /dev/null +++ b/modules/storages/app/components/storages/admin/sidebar/validation_result_component.html.erb @@ -0,0 +1,61 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2024 the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + +<%= + component_wrapper do + flex_layout do |container| + container.with_row do + render(Primer::Beta::Text.new(font_weight: :bold, test_selector: "validation-result--subtitle")) do + I18n.t("storages.health.connection_validation.subtitle") + end + end + + if @result.validation_result_exists? + container.with_row(mt: 2) do + status = status_indicator + + concat(render(Primer::Beta::Text.new(pr: 2, test_selector: "validation-result--timestamp")) do + I18n.t('storages.health.checked', datetime: helpers.format_time(@result.timestamp)) + end) + concat(render(Primer::Beta::Label.new(scheme: status[:scheme])) { status[:label] }) + end + end + + if @result.description.present? + prefix = @result.error_code? ? "#{@result.error_code.upcase}: " : "" + + container.with_row(mt: 2) do + render(Primer::Beta::Text.new(color: :muted, test_selector: "validation-result--description")) do + prefix + @result.description + end + end + end + end + end +%> diff --git a/app/components/work_packages/share/concerns/displayable_roles.rb b/modules/storages/app/components/storages/admin/sidebar/validation_result_component.rb similarity index 56% rename from app/components/work_packages/share/concerns/displayable_roles.rb rename to modules/storages/app/components/storages/admin/sidebar/validation_result_component.rb index ff0faa338347..f145051b2929 100644 --- a/app/components/work_packages/share/concerns/displayable_roles.rb +++ b/modules/storages/app/components/storages/admin/sidebar/validation_result_component.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -# -- copyright +#-- copyright # OpenProject is an open source project management software. -# Copyright (C) 2023 the OpenProject GmbH +# Copyright (C) 2012-2024 the OpenProject GmbH # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License version 3. @@ -26,24 +26,33 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # See COPYRIGHT and LICENSE files for more details. -# ++ +#++ -module WorkPackages - module Share - module Concerns - module DisplayableRoles - def options - [ - { label: I18n.t("work_package.sharing.permissions.edit"), - value: Role::BUILTIN_WORK_PACKAGE_EDITOR, - description: I18n.t("work_package.sharing.permissions.edit_description") }, - { label: I18n.t("work_package.sharing.permissions.comment"), - value: Role::BUILTIN_WORK_PACKAGE_COMMENTER, - description: I18n.t("work_package.sharing.permissions.comment_description") }, - { label: I18n.t("work_package.sharing.permissions.view"), - value: Role::BUILTIN_WORK_PACKAGE_VIEWER, - description: I18n.t("work_package.sharing.permissions.view_description") } - ] +module Storages + module Admin + module Sidebar + class ValidationResultComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(result:) + super(result) + @result = result + end + + private + + def status_indicator + case @result.type + when :healthy + { scheme: :success, label: I18n.t("storages.health.label_healthy") } + when :warning + { scheme: :attention, label: I18n.t("storages.health.label_warning") } + when :error + { scheme: :danger, label: I18n.t("storages.health.label_error") } + else + raise ArgumentError, "Unknown validation result type for status indicator: #{@result.type}" + end end end end diff --git a/modules/storages/app/components/storages/admin/sidebar_component.html.erb b/modules/storages/app/components/storages/admin/sidebar_component.html.erb index 3b19f1e32f1b..918d19b50396 100644 --- a/modules/storages/app/components/storages/admin/sidebar_component.html.erb +++ b/modules/storages/app/components/storages/admin/sidebar_component.html.erb @@ -2,7 +2,10 @@ component_wrapper do render(Primer::OpenProject::BorderGrid.new) do |border_grid| border_grid.with_row { render(Storages::Admin::Sidebar::HealthStatusComponent.new(storage: @storage)) } - border_grid.with_row { render(Storages::Admin::Sidebar::HealthNotificationsComponent.new(storage: @storage)) } + + if @storage.automatic_management_enabled? + border_grid.with_row { render(Storages::Admin::Sidebar::HealthNotificationsComponent.new(storage: @storage)) } + end end end %> diff --git a/modules/storages/app/controllers/storages/admin/connection_validation_controller.rb b/modules/storages/app/controllers/storages/admin/connection_validation_controller.rb new file mode 100644 index 000000000000..fd758c0ea165 --- /dev/null +++ b/modules/storages/app/controllers/storages/admin/connection_validation_controller.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Storages + module Admin + class ConnectionValidationController < ApplicationController + include OpTurbo::ComponentStream + + layout "admin" + + before_action :require_admin + + model_object OneDriveStorage + + before_action :find_model_object, only: %i[validate_connection] + + def validate_connection + @result = Peripherals::OneDriveConnectionValidator + .new(storage: @storage) + .validate + update_via_turbo_stream(component: Sidebar::ValidationResultComponent.new(result: @result)) + respond_to_with_turbo_streams + end + + private + + def find_model_object(object_id = :storage_id) + super + @storage = @object + end + end + end +end diff --git a/app/components/work_packages/share/concerns/authorization.rb b/modules/storages/app/models/storages/connection_validation.rb similarity index 76% rename from app/components/work_packages/share/concerns/authorization.rb rename to modules/storages/app/models/storages/connection_validation.rb index 4a927c0b5983..a9ad82f087d8 100644 --- a/app/components/work_packages/share/concerns/authorization.rb +++ b/modules/storages/app/models/storages/connection_validation.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -# -- copyright +#-- copyright # OpenProject is an open source project management software. -# Copyright (C) 2023 the OpenProject GmbH +# Copyright (C) 2012-2024 the OpenProject GmbH # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License version 3. @@ -26,20 +26,16 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # See COPYRIGHT and LICENSE files for more details. -# ++ +#++ -module WorkPackages - module Share - module Concerns - module Authorization - extend ActiveSupport::Concern +module Storages + ConnectionValidation = Data.define(:type, :error_code, :description, :timestamp) do + def validation_result_exists? + type.present? && type != :none + end - included do - def sharing_manageable? - User.current.allowed_in_project?(:share_work_packages, @work_package.project) - end - end - end + def error_code? + error_code.present? && error_code != :none end end end diff --git a/modules/storages/app/models/storages/storage.rb b/modules/storages/app/models/storages/storage.rb index 2e4a06eeda75..07e02e575ab6 100644 --- a/modules/storages/app/models/storages/storage.rb +++ b/modules/storages/app/models/storages/storage.rb @@ -83,6 +83,8 @@ class Storage < ApplicationRecord scope :automatic_management_enabled, -> { where("provider_fields->>'automatically_managed' = 'true'") } + scope :in_project, ->(project_id) { joins(project_storages: :project).where(project_storages: { project_id: }) } + enum health_status: { pending: "pending", healthy: "healthy", diff --git a/modules/storages/app/services/storages/nextcloud_group_folder_properties_sync_service.rb b/modules/storages/app/services/storages/nextcloud_group_folder_properties_sync_service.rb index db20f46f6102..508fa6b8acab 100644 --- a/modules/storages/app/services/storages/nextcloud_group_folder_properties_sync_service.rb +++ b/modules/storages/app/services/storages/nextcloud_group_folder_properties_sync_service.rb @@ -187,10 +187,11 @@ def ensure_folders_exist(remote_folders) current_path = id_folder_map[project_storage.project_folder_id] if current_path != project_storage.managed_project_folder_path - rename_folder(project_storage, current_path).on_failure do |service_result| + target_folder_name = name_from_path(project_storage.managed_project_folder_path) + rename_folder(project_storage.project_folder_id, target_folder_name).on_failure do |service_result| format_and_log_error(service_result.errors, - source: current_path, - target: project_storage.managed_project_folder_path) + folder_id: project_storage.project_folder_id, + folder_name: target_folder_name) # we need to stop as this would mess with the other processes return service_result @@ -202,10 +203,14 @@ def ensure_folders_exist(remote_folders) ServiceResult.success end - def rename_folder(project_storage, current_name) + def name_from_path(path) + path.split("/").last + end + + def rename_folder(folder_id, folder_name) Peripherals::Registry .resolve("nextcloud.commands.rename_file") - .call(storage: @storage, source: current_name, target: project_storage.managed_project_folder_path) + .call(storage: @storage, auth_strategy:, file_id: folder_id, name: folder_name) end def create_folder(project_storage) @@ -289,7 +294,7 @@ def client_tokens_scope end def auth_strategy - Peripherals::StorageInteraction::AuthenticationStrategies::BasicAuth.strategy + Peripherals::Registry.resolve("nextcloud.authentication.userless").call end def admin_client_tokens_scope diff --git a/modules/storages/app/services/storages/one_drive_managed_folder_sync_service.rb b/modules/storages/app/services/storages/one_drive_managed_folder_sync_service.rb index e025c018a53c..3a656d6f0c08 100644 --- a/modules/storages/app/services/storages/one_drive_managed_folder_sync_service.rb +++ b/modules/storages/app/services/storages/one_drive_managed_folder_sync_service.rb @@ -107,11 +107,11 @@ def set_permissions(path, permissions) end end - def rename_folder(source, target) + def rename_folder(folder_id, folder_name) Peripherals::Registry .resolve("one_drive.commands.rename_file") - .call(storage: @storage, source:, target:) - .result_or { |error| format_and_log_error(error, source:, target:) } + .call(storage: @storage, auth_strategy:, file_id: folder_id, name: folder_name) + .result_or { |error| format_and_log_error(error, folder_id:, folder_name:) } end # rubocop:disable Metrics/AbcSize 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 00d51b85bf16..1b508503b859 100644 --- a/modules/storages/app/services/storages/project_storages/create_service.rb +++ b/modules/storages/app/services/storages/project_storages/create_service.rb @@ -39,7 +39,8 @@ def after_perform(service_call) add_historical_data(service_call) if project_folder_mode != :inactive OpenProject::Notifications.send( OpenProject::Events::PROJECT_STORAGE_CREATED, - project_folder_mode: + project_folder_mode:, + storage: project_storage.storage ) service_call 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 6538a78e45e1..afd7cbb34d9b 100644 --- a/modules/storages/app/services/storages/project_storages/delete_service.rb +++ b/modules/storages/app/services/storages/project_storages/delete_service.rb @@ -50,7 +50,8 @@ def persist(service_result) delete_associated_file_links OpenProject::Notifications.send( OpenProject::Events::PROJECT_STORAGE_DESTROYED, - project_folder_mode: deletion_result.result.project_folder_mode.to_sym + project_folder_mode: deletion_result.result.project_folder_mode.to_sym, + storage: deletion_result.result.storage ) 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 da5f97e1045f..5f39e3ea30b0 100644 --- a/modules/storages/app/services/storages/project_storages/update_service.rb +++ b/modules/storages/app/services/storages/project_storages/update_service.rb @@ -40,7 +40,8 @@ def after_perform(service_call) add_historical_data(service_call) if project_folder_mode != :inactive OpenProject::Notifications.send( OpenProject::Events::PROJECT_STORAGE_UPDATED, - project_folder_mode: + project_folder_mode:, + storage: project_storage.storage ) service_call 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 ecda5f36208a..b5d320e8349a 100644 --- a/modules/storages/app/views/storages/admin/storages/edit.html.erb +++ b/modules/storages/app/views/storages/admin/storages/edit.html.erb @@ -64,7 +64,8 @@ See COPYRIGHT and LICENSE files for more details. <% end %> <% end %> -<% if @storage.automatic_management_enabled? %> +<% display_sidebar = @storage.provider_type_one_drive? || @storage.automatic_management_enabled? %> +<% if display_sidebar %> <%= render(Primer::Alpha::Layout.new(stacking_breakpoint: :lg)) do |component| %> <% component.with_main() do %> <%= render(::Storages::Admin::StorageViewComponent.new(@storage)) %> diff --git a/modules/storages/app/workers/storages/automatically_managed_storage_sync_job.rb b/modules/storages/app/workers/storages/automatically_managed_storage_sync_job.rb new file mode 100644 index 000000000000..ef942bd6790a --- /dev/null +++ b/modules/storages/app/workers/storages/automatically_managed_storage_sync_job.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Storages + class AutomaticallyManagedStorageSyncJob < ApplicationJob + include GoodJob::ActiveJobExtensions::Concurrency + extend ::DebounceableJob + + queue_with_priority :above_normal + + good_job_control_concurrency_with( + total_limit: 2, + enqueue_limit: 1, + perform_limit: 1, + key: -> { "StorageSyncJob-#{arguments.last.short_provider_type}-#{arguments.last.id}" } + ) + + retry_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError, wait: 5, attempts: 10 + + retry_on Errors::IntegrationJobError, attempts: 5 do |job, error| + if job.executions >= 5 + OpenProject::Notifications.send( + OpenProject::Events::STORAGE_TURNED_UNHEALTHY, storage: job.arguments.last, reason: error.message + ) + end + end + + def self.key(storage) = "sync-#{storage.short_provider_type}-#{storage.id}" + + def perform(storage) + return unless storage.configured? && storage.automatically_managed? + + sync_result = case storage.short_provider_type + when "nextcloud" + NextcloudGroupFolderPropertiesSyncService.call(storage) + when "one_drive" + OneDriveManagedFolderSyncService.call(storage) + else + raise "Unknown Storage Type" + end + + sync_result.on_failure { raise Errors::IntegrationJobError, sync_result.errors.to_s } + sync_result.on_success { OpenProject::Notifications.send(OpenProject::Events::STORAGE_TURNED_HEALTHY, storage:) } + end + end +end diff --git a/modules/storages/app/workers/storages/manage_storage_integrations_job.rb b/modules/storages/app/workers/storages/manage_storage_integrations_job.rb index 1588ab8b08ce..48e9d5129c45 100644 --- a/modules/storages/app/workers/storages/manage_storage_integrations_job.rb +++ b/modules/storages/app/workers/storages/manage_storage_integrations_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) 2012-2024 the OpenProject GmbH @@ -29,17 +31,7 @@ module Storages class ManageStorageIntegrationsJob < ApplicationJob include GoodJob::ActiveJobExtensions::Concurrency - using ::Storages::Peripherals::ServiceResultRefinements - - retry_on ::Storages::Errors::IntegrationJobError, attempts: 5 do |job, errors| - if job.executions >= 5 - OpenProject::Notifications.send( - OpenProject::Events::STORAGE_TURNED_UNHEALTHY, - storage: errors.storage, - reason: errors.errors.to_s - ) - end - end + extend ::DebounceableJob good_job_control_concurrency_with( total_limit: 2, @@ -48,35 +40,15 @@ class ManageStorageIntegrationsJob < ApplicationJob ) retry_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError, - wait: 5.minutes, - attempts: 3 + wait: 5, + attempts: 20 - SINGLE_THREAD_DEBOUNCE_TIME = 4.seconds.freeze KEY = :manage_nextcloud_integration_job_debounce_happened_at CRON_JOB_KEY = :"Storages::ManageStorageIntegrationsJob" queue_with_priority :above_normal class << self - def debounce - if debounce_happened_in_current_thread_recently? - false - else - # TODO: - # Why there is 5 seconds delay? - # it is like that because for 1 thread and if there is no delay more than - # SINGLE_THREAD_DEBOUNCE_TIME(4.seconds) - # then some events can be lost - # - # Possibly "true" solutions are: - # 1. have after_request middleware to schedule one job after a request cycle - # 2. use concurrent ruby to have 'true' debounce. - result = set(wait: 5.seconds).perform_later - RequestStore.store[KEY] = Time.current - result - end - end - def disable_cron_job_if_needed if ::Storages::ProjectStorage.active_automatically_managed.exists? GoodJob::Setting.cron_key_enable(CRON_JOB_KEY) unless GoodJob::Setting.cron_key_enabled?(CRON_JOB_KEY) @@ -85,42 +57,13 @@ def disable_cron_job_if_needed end end - private - - def debounce_happened_in_current_thread_recently? - timestamp = RequestStore.store[KEY] - timestamp.present? && (timestamp + SINGLE_THREAD_DEBOUNCE_TIME) > Time.current - end + def key = KEY end def perform - find_storages do |storage| - next unless storage.configured? - - result = service_for(storage).call(storage) - result.match( - on_success: ->(_) { OpenProject::Notifications.send(OpenProject::Events::STORAGE_TURNED_HEALTHY, storage:) }, - on_failure: ->(errors) do - raise ::Storages::Errors::IntegrationJobError.new(storage:, errors:) - end - ) + Storage.automatic_management_enabled.find_each do |storage| + AutomaticallyManagedStorageSyncJob.perform_later(storage) end end - - private - - def find_storages(&) - ::Storages::Storage - .automatic_management_enabled - .includes(:oauth_client) - .find_each(&) - end - - def service_for(storage) - return NextcloudGroupFolderPropertiesSyncService if storage.provider_type_nextcloud? - return OneDriveManagedFolderSyncService if storage.provider_type_one_drive? - - raise "Unknown Storage" - end end end diff --git a/modules/storages/config/locales/crowdin/af.yml b/modules/storages/config/locales/crowdin/af.yml index d6652abb4e6f..c8922439c45c 100644 --- a/modules/storages/config/locales/crowdin/af.yml +++ b/modules/storages/config/locales/crowdin/af.yml @@ -102,11 +102,24 @@ af: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Fout label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/ar.yml b/modules/storages/config/locales/crowdin/ar.yml index de7c3b6ab1ce..e8fd9693abf6 100644 --- a/modules/storages/config/locales/crowdin/ar.yml +++ b/modules/storages/config/locales/crowdin/ar.yml @@ -102,11 +102,24 @@ ar: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: خطأ label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/az.yml b/modules/storages/config/locales/crowdin/az.yml index f009b4d19cb8..a6684ecce9b2 100644 --- a/modules/storages/config/locales/crowdin/az.yml +++ b/modules/storages/config/locales/crowdin/az.yml @@ -102,11 +102,24 @@ az: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/be.yml b/modules/storages/config/locales/crowdin/be.yml index a8296171318f..7af5aa6c88db 100644 --- a/modules/storages/config/locales/crowdin/be.yml +++ b/modules/storages/config/locales/crowdin/be.yml @@ -102,11 +102,24 @@ be: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/bg.yml b/modules/storages/config/locales/crowdin/bg.yml index 2ff42d9e63c2..a529372fbabd 100644 --- a/modules/storages/config/locales/crowdin/bg.yml +++ b/modules/storages/config/locales/crowdin/bg.yml @@ -102,11 +102,24 @@ bg: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Грешка label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/ca.yml b/modules/storages/config/locales/crowdin/ca.yml index 4b8656aea17a..5582691f0fd7 100644 --- a/modules/storages/config/locales/crowdin/ca.yml +++ b/modules/storages/config/locales/crowdin/ca.yml @@ -102,11 +102,24 @@ ca: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Healthy label_pending: Pendent + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/ckb-IR.yml b/modules/storages/config/locales/crowdin/ckb-IR.yml index a535a36cec40..3555a9ec9305 100644 --- a/modules/storages/config/locales/crowdin/ckb-IR.yml +++ b/modules/storages/config/locales/crowdin/ckb-IR.yml @@ -102,11 +102,24 @@ ckb-IR: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/cs.yml b/modules/storages/config/locales/crowdin/cs.yml index 1d885a18010a..40b8ab917ee0 100644 --- a/modules/storages/config/locales/crowdin/cs.yml +++ b/modules/storages/config/locales/crowdin/cs.yml @@ -102,11 +102,24 @@ cs: storage_provider: Poskytovatel úložiště health: checked: Poslední kontrola %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Chyba label_healthy: Zdravé label_pending: Nevyřízeno + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: od %{datetime} - subtitle: Automaticky spravované projektové složky title: Zdravotní stav health_email_notifications: description_subscribed: Všichni administrátoři dostávají e-mailová oznámení o zdravotním stavu pro toto úložiště. diff --git a/modules/storages/config/locales/crowdin/da.yml b/modules/storages/config/locales/crowdin/da.yml index 22adf5d64004..0ac6ee239334 100644 --- a/modules/storages/config/locales/crowdin/da.yml +++ b/modules/storages/config/locales/crowdin/da.yml @@ -102,11 +102,24 @@ da: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Fejl label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/de.yml b/modules/storages/config/locales/crowdin/de.yml index 8b6571393c69..d5c55f4c2e58 100644 --- a/modules/storages/config/locales/crowdin/de.yml +++ b/modules/storages/config/locales/crowdin/de.yml @@ -102,11 +102,24 @@ de: storage_provider: Speicheranbieter health: checked: Zuletzt geprüft %{datetime} + connection_validation: + action: Verbindung erneut überprüfen + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Fehler label_healthy: Fehlerfrei label_pending: Ausstehend + label_warning: Warnung + project_folders: + subtitle: Automatisch verwaltete Projektordner since: seit %{datetime} - subtitle: Automatisch verwaltete Projektordner title: Verbindungsstatus health_email_notifications: description_subscribed: Alle Administratoren erhalten E-Mail-Benachrichtigungen über den Zustand dieses Dateispeichers. diff --git a/modules/storages/config/locales/crowdin/el.yml b/modules/storages/config/locales/crowdin/el.yml index aec14cd251cb..34a8deecc0aa 100644 --- a/modules/storages/config/locales/crowdin/el.yml +++ b/modules/storages/config/locales/crowdin/el.yml @@ -102,11 +102,24 @@ el: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Σφάλμα label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/eo.yml b/modules/storages/config/locales/crowdin/eo.yml index aea69c3b46e1..1712fdb42056 100644 --- a/modules/storages/config/locales/crowdin/eo.yml +++ b/modules/storages/config/locales/crowdin/eo.yml @@ -102,11 +102,24 @@ eo: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Eraro label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/es.yml b/modules/storages/config/locales/crowdin/es.yml index b17d67e5f71f..9b6634b3073a 100644 --- a/modules/storages/config/locales/crowdin/es.yml +++ b/modules/storages/config/locales/crowdin/es.yml @@ -102,11 +102,24 @@ es: storage_provider: Proveedor de almacenamiento health: checked: Última comprobación %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Correcto label_pending: Pendiente + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: desde %{datetime} - subtitle: Carpetas de proyecto gestionadas automáticamente title: Estado de salud health_email_notifications: description_subscribed: Todos los administradores reciben notificaciones por correo electrónico sobre el estado de salud de este almacenamiento. diff --git a/modules/storages/config/locales/crowdin/et.yml b/modules/storages/config/locales/crowdin/et.yml index 7fa226b54c58..2a7c7f94efc6 100644 --- a/modules/storages/config/locales/crowdin/et.yml +++ b/modules/storages/config/locales/crowdin/et.yml @@ -102,11 +102,24 @@ et: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Tõrge label_healthy: Healthy label_pending: Ootel + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/eu.yml b/modules/storages/config/locales/crowdin/eu.yml index aaacf656239a..75b9f0af4c75 100644 --- a/modules/storages/config/locales/crowdin/eu.yml +++ b/modules/storages/config/locales/crowdin/eu.yml @@ -102,11 +102,24 @@ eu: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/fa.yml b/modules/storages/config/locales/crowdin/fa.yml index 6381e2b8058c..c562f473fc8f 100644 --- a/modules/storages/config/locales/crowdin/fa.yml +++ b/modules/storages/config/locales/crowdin/fa.yml @@ -102,11 +102,24 @@ fa: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/fi.yml b/modules/storages/config/locales/crowdin/fi.yml index 0dc962e96b90..0133a8ec165b 100644 --- a/modules/storages/config/locales/crowdin/fi.yml +++ b/modules/storages/config/locales/crowdin/fi.yml @@ -102,11 +102,24 @@ fi: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Virhe label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/fil.yml b/modules/storages/config/locales/crowdin/fil.yml index b68d2ddb2f6c..73a6efa36782 100644 --- a/modules/storages/config/locales/crowdin/fil.yml +++ b/modules/storages/config/locales/crowdin/fil.yml @@ -102,11 +102,24 @@ fil: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Mali label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/fr.yml b/modules/storages/config/locales/crowdin/fr.yml index 37cdecfb5ed4..8c5ed198e789 100644 --- a/modules/storages/config/locales/crowdin/fr.yml +++ b/modules/storages/config/locales/crowdin/fr.yml @@ -102,11 +102,24 @@ fr: storage_provider: Fournisseur de stockage health: checked: Dernière vérification %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Erreur label_healthy: Sain label_pending: En attente + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: depuis %{datetime} - subtitle: Dossiers de projets gérés automatiquement title: État de santé health_email_notifications: description_subscribed: Tous les administrateurs reçoivent des notifications par e-mail concernant le statut de cet espace de stockage. diff --git a/modules/storages/config/locales/crowdin/he.yml b/modules/storages/config/locales/crowdin/he.yml index f749df3c75e4..82cd5a8d5608 100644 --- a/modules/storages/config/locales/crowdin/he.yml +++ b/modules/storages/config/locales/crowdin/he.yml @@ -102,11 +102,24 @@ he: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: שגיאה label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/hi.yml b/modules/storages/config/locales/crowdin/hi.yml index b5569f22d94e..5c551155c0b2 100644 --- a/modules/storages/config/locales/crowdin/hi.yml +++ b/modules/storages/config/locales/crowdin/hi.yml @@ -102,11 +102,24 @@ hi: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: त्रुटि label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/hr.yml b/modules/storages/config/locales/crowdin/hr.yml index b2b3c2f809f8..6a928df911b6 100644 --- a/modules/storages/config/locales/crowdin/hr.yml +++ b/modules/storages/config/locales/crowdin/hr.yml @@ -102,11 +102,24 @@ hr: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Greška label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/hu.yml b/modules/storages/config/locales/crowdin/hu.yml index 0e7bd14b57d2..5ed9dc470047 100644 --- a/modules/storages/config/locales/crowdin/hu.yml +++ b/modules/storages/config/locales/crowdin/hu.yml @@ -102,11 +102,24 @@ hu: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Hiba label_healthy: Healthy label_pending: Függő + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/id.yml b/modules/storages/config/locales/crowdin/id.yml index 8b99298f228e..9ca5ec976561 100644 --- a/modules/storages/config/locales/crowdin/id.yml +++ b/modules/storages/config/locales/crowdin/id.yml @@ -102,11 +102,24 @@ id: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Eror label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/it.yml b/modules/storages/config/locales/crowdin/it.yml index 9f5248113a4f..d420dd677167 100644 --- a/modules/storages/config/locales/crowdin/it.yml +++ b/modules/storages/config/locales/crowdin/it.yml @@ -102,11 +102,24 @@ it: storage_provider: Fornitore di archiviazione health: checked: Ultimo controllo %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Errore label_healthy: Tutto ok label_pending: In sospeso + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: dal giorno %{datetime} - subtitle: Cartelle di progetto gestite automaticamente title: Stato di salute health_email_notifications: description_subscribed: Tutti gli amministratori ricevono notifiche e-mail sullo stato di salute di questo archivio. diff --git a/modules/storages/config/locales/crowdin/ja.yml b/modules/storages/config/locales/crowdin/ja.yml index a5b13ef3c3ae..a285d8b22849 100644 --- a/modules/storages/config/locales/crowdin/ja.yml +++ b/modules/storages/config/locales/crowdin/ja.yml @@ -102,11 +102,24 @@ ja: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: エラー! label_healthy: Healthy label_pending: 保留中 + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/ka.yml b/modules/storages/config/locales/crowdin/ka.yml index a8f9c2ed4cdd..3fefd8e768de 100644 --- a/modules/storages/config/locales/crowdin/ka.yml +++ b/modules/storages/config/locales/crowdin/ka.yml @@ -102,11 +102,24 @@ ka: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: შეცდომა label_healthy: ჯანმრთელი label_pending: დარჩენილია + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/kk.yml b/modules/storages/config/locales/crowdin/kk.yml index 8deecece1663..9694407830ce 100644 --- a/modules/storages/config/locales/crowdin/kk.yml +++ b/modules/storages/config/locales/crowdin/kk.yml @@ -102,11 +102,24 @@ kk: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/ko.yml b/modules/storages/config/locales/crowdin/ko.yml index fc4f3b821f6f..3f028929c961 100644 --- a/modules/storages/config/locales/crowdin/ko.yml +++ b/modules/storages/config/locales/crowdin/ko.yml @@ -102,11 +102,24 @@ ko: storage_provider: 저장소 공급자 health: checked: '마지막 확인: %{datetime}' + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: 오류 label_healthy: 정상 label_pending: 대기 중 + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: '%{datetime} 이후' - subtitle: 자동으로 관리되는 프로젝트 폴더 title: 상태 health_email_notifications: description_subscribed: 모든 관리자는 이 저장소의 상태 이메일 알림을 받습니다. diff --git a/modules/storages/config/locales/crowdin/lt.yml b/modules/storages/config/locales/crowdin/lt.yml index 2980105a6485..7395e4e23595 100644 --- a/modules/storages/config/locales/crowdin/lt.yml +++ b/modules/storages/config/locales/crowdin/lt.yml @@ -102,11 +102,24 @@ lt: storage_provider: Saugyklos tiekėjas health: checked: Paskutinį kartą tikrinta %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Klaida label_healthy: Sveikas label_pending: Laukiama + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: nuo %{datetime} - subtitle: Automatiškai valdomi projekto aplankai title: Sveikatos būsena health_email_notifications: description_subscribed: Visi administratoriai gauna šios saugyklos sveikatos būsenos pranešimus e-paštu. diff --git a/modules/storages/config/locales/crowdin/lv.yml b/modules/storages/config/locales/crowdin/lv.yml index 9d0b650cb05a..ca07369aa984 100644 --- a/modules/storages/config/locales/crowdin/lv.yml +++ b/modules/storages/config/locales/crowdin/lv.yml @@ -102,11 +102,24 @@ lv: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/mn.yml b/modules/storages/config/locales/crowdin/mn.yml index 0030403b079b..66058b7a3499 100644 --- a/modules/storages/config/locales/crowdin/mn.yml +++ b/modules/storages/config/locales/crowdin/mn.yml @@ -102,11 +102,24 @@ mn: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/ms.yml b/modules/storages/config/locales/crowdin/ms.yml index 61daaa0e1840..ba8ee51c9d47 100644 --- a/modules/storages/config/locales/crowdin/ms.yml +++ b/modules/storages/config/locales/crowdin/ms.yml @@ -46,7 +46,7 @@ ms: permission_delete_files_explanation: Kebenaran ini hanya tersedia untuk storan Nextcloud permission_header_for_project_module_storages: Folder projek yang dikendalikan secara automatik permission_manage_file_links: Urus pautan fail - permission_manage_files_in_project: Manage files in project + permission_manage_files_in_project: Uruskan fail dalam projek permission_read_files: 'Folder projek yang dikendalikan secara automatik: Baca fail' permission_share_files: 'Folder projek yang dikendalikan secara automatik: Kongsi fail' permission_share_files_explanation: Kebenaran ini hanya tersedia untuk storan Nextcloud @@ -102,11 +102,24 @@ ms: storage_provider: Penyedia storan health: checked: Terakhir disemak %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Ralat label_healthy: Sihat label_pending: Dalam proses + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: sejak %{datetime} - subtitle: Folder projek yang dikendalikan secara automatik title: Status kesihatan health_email_notifications: description_subscribed: Semua pentadbir menerima pemberitahuan e-mel status kesihatan bagi storan ini. diff --git a/modules/storages/config/locales/crowdin/ne.yml b/modules/storages/config/locales/crowdin/ne.yml index 4c5a4a5cf94c..efcc074bf1b8 100644 --- a/modules/storages/config/locales/crowdin/ne.yml +++ b/modules/storages/config/locales/crowdin/ne.yml @@ -102,11 +102,24 @@ ne: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/nl.yml b/modules/storages/config/locales/crowdin/nl.yml index ea6d5cb199df..25291d9706b7 100644 --- a/modules/storages/config/locales/crowdin/nl.yml +++ b/modules/storages/config/locales/crowdin/nl.yml @@ -102,11 +102,24 @@ nl: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Fout label_healthy: Gezond label_pending: Lopende + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: sinds %{datetime} - subtitle: Automatisch beheerde projectmappen title: Gezondheidsstatus health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/no.yml b/modules/storages/config/locales/crowdin/no.yml index 38806fc91d26..3e71f60ea932 100644 --- a/modules/storages/config/locales/crowdin/no.yml +++ b/modules/storages/config/locales/crowdin/no.yml @@ -102,11 +102,24 @@ storage_provider: Lagringsleverandør health: checked: Sist sjekket %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Feil label_healthy: Sunn label_pending: Ventende + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: siden %{datetime} - subtitle: Automatisk administrerte prosjektmapper title: Helse-status health_email_notifications: description_subscribed: Alle administratorer mottar e-postmeldinger med helsestatus for denne lagringen. diff --git a/modules/storages/config/locales/crowdin/pl.yml b/modules/storages/config/locales/crowdin/pl.yml index 93f2826a7dec..dd36ef9d74c6 100644 --- a/modules/storages/config/locales/crowdin/pl.yml +++ b/modules/storages/config/locales/crowdin/pl.yml @@ -102,11 +102,24 @@ pl: storage_provider: Dostawca magazynu health: checked: Ostatnio sprawdzono %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Błąd label_healthy: Zdrowe label_pending: Oczekujący + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: od %{datetime} - subtitle: Automatycznie zarządzane foldery projektu title: Status kondycji health_email_notifications: description_subscribed: Wszyscy administratorzy otrzymują powiadomienia e-mail o kondycji tego magazynu. diff --git a/modules/storages/config/locales/crowdin/pt-BR.yml b/modules/storages/config/locales/crowdin/pt-BR.yml index a816674b3173..7280b9585660 100644 --- a/modules/storages/config/locales/crowdin/pt-BR.yml +++ b/modules/storages/config/locales/crowdin/pt-BR.yml @@ -102,11 +102,24 @@ pt-BR: storage_provider: Provedor de armazenamento health: checked: Última verificação em %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Erro label_healthy: Saudável label_pending: Pendente + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: desde %{datetime} - subtitle: Pastas do projeto gerenciadas automaticamente title: Status de saúde health_email_notifications: description_subscribed: Todos os administradores recebem notificações por e-mail sobre o estado de saúde deste armazenamento. diff --git a/modules/storages/config/locales/crowdin/pt-PT.yml b/modules/storages/config/locales/crowdin/pt-PT.yml index 5ae18134e69f..5f49fb93fca6 100644 --- a/modules/storages/config/locales/crowdin/pt-PT.yml +++ b/modules/storages/config/locales/crowdin/pt-PT.yml @@ -102,11 +102,24 @@ pt-PT: storage_provider: Fornecedor de armazenamento health: checked: Última verificação em %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Erro label_healthy: Bom estado label_pending: Pendente + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: desde %{datetime} - subtitle: Pastas do projeto geridas automaticamente title: Estado de funcionamento atual health_email_notifications: description_subscribed: Todos os administradores recebem notificações por e-mail sobre o estado de funcionamento deste armazenamento. diff --git a/modules/storages/config/locales/crowdin/ro.yml b/modules/storages/config/locales/crowdin/ro.yml index ba7a9ca1ab1e..21cbf50abdd6 100644 --- a/modules/storages/config/locales/crowdin/ro.yml +++ b/modules/storages/config/locales/crowdin/ro.yml @@ -102,11 +102,24 @@ ro: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Eroare label_healthy: Healthy label_pending: în așteptare + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/ru.yml b/modules/storages/config/locales/crowdin/ru.yml index 738ffc0dbc80..934e30045904 100644 --- a/modules/storages/config/locales/crowdin/ru.yml +++ b/modules/storages/config/locales/crowdin/ru.yml @@ -102,11 +102,24 @@ ru: storage_provider: Поставщик хранилища health: checked: Последняя проверка %{datetime} + connection_validation: + action: Перепроверьте соединение + client_id_wrong: Настроенный идентификатор клиента OAuth 2 недействителен. Пожалуйста, проверьте конфигурацию. + client_secret_wrong: Настроенный ключ клиента OAuth 2 недействителен. Пожалуйста, проверьте конфигурацию. + drive_id_wrong: Настроенный идентификатор диска не найден. Пожалуйста, проверьте конфигурацию. + not_configured: Соединение не удалось подтвердить. Пожалуйста, сначала завершите настройку. + placeholder: Проверьте подключение к серверу. + subtitle: Проверка соединения + tenant_id_wrong: Настроенный идентификатор каталога недействителен. Пожалуйста, проверьте конфигурацию. + unexpected_content: На диске обнаружено неожидаемое содержимое. + unknown_error: Соединение не удалось подтвердить. Произошла неизвестная ошибка. Пожалуйста, проверьте журналы сервера для получения дополнительной информации. label_error: Ошибка label_healthy: Здоровые label_pending: В ожидании + label_warning: Предупреждение + project_folders: + subtitle: Автоматически управляемые папки проекта since: с %{datetime} - subtitle: Автоматически управляемые папки проектов title: Состояние здоровья health_email_notifications: description_subscribed: Все администраторы получают уведомления о состоянии здоровья этого хранилища. diff --git a/modules/storages/config/locales/crowdin/rw.yml b/modules/storages/config/locales/crowdin/rw.yml index 7c6122cb579c..4f702a4592e3 100644 --- a/modules/storages/config/locales/crowdin/rw.yml +++ b/modules/storages/config/locales/crowdin/rw.yml @@ -102,11 +102,24 @@ rw: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/si.yml b/modules/storages/config/locales/crowdin/si.yml index 32a67d521e54..11b47e0b8288 100644 --- a/modules/storages/config/locales/crowdin/si.yml +++ b/modules/storages/config/locales/crowdin/si.yml @@ -102,11 +102,24 @@ si: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: දෝෂය label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/sk.yml b/modules/storages/config/locales/crowdin/sk.yml index f18f7475fabc..6aca1e106a21 100644 --- a/modules/storages/config/locales/crowdin/sk.yml +++ b/modules/storages/config/locales/crowdin/sk.yml @@ -102,11 +102,24 @@ sk: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Chyba label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/sl.yml b/modules/storages/config/locales/crowdin/sl.yml index d5def69abb7a..6d6f9fc2fc69 100644 --- a/modules/storages/config/locales/crowdin/sl.yml +++ b/modules/storages/config/locales/crowdin/sl.yml @@ -102,11 +102,24 @@ sl: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Napaka label_healthy: Healthy label_pending: Čakajoče + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/sr.yml b/modules/storages/config/locales/crowdin/sr.yml index ed4d3c36874c..7280c53ea74f 100644 --- a/modules/storages/config/locales/crowdin/sr.yml +++ b/modules/storages/config/locales/crowdin/sr.yml @@ -102,11 +102,24 @@ sr: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/sv.yml b/modules/storages/config/locales/crowdin/sv.yml index 767bb497892d..9994b31430ef 100644 --- a/modules/storages/config/locales/crowdin/sv.yml +++ b/modules/storages/config/locales/crowdin/sv.yml @@ -102,11 +102,24 @@ sv: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Fel label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/th.yml b/modules/storages/config/locales/crowdin/th.yml index 5318307d870b..e9e3fdcdca6c 100644 --- a/modules/storages/config/locales/crowdin/th.yml +++ b/modules/storages/config/locales/crowdin/th.yml @@ -102,11 +102,24 @@ th: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: ข้อผิดพลาด label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/tr.yml b/modules/storages/config/locales/crowdin/tr.yml index 98f6b917f58f..fbd1434a43fb 100644 --- a/modules/storages/config/locales/crowdin/tr.yml +++ b/modules/storages/config/locales/crowdin/tr.yml @@ -102,11 +102,24 @@ tr: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Hata label_healthy: Healthy label_pending: Bekliyor + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/uk.yml b/modules/storages/config/locales/crowdin/uk.yml index cfc05c0644a3..b12b98e9f505 100644 --- a/modules/storages/config/locales/crowdin/uk.yml +++ b/modules/storages/config/locales/crowdin/uk.yml @@ -102,11 +102,24 @@ uk: storage_provider: Постачальник сховища health: checked: 'Востаннє перевірено: %{datetime}' + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Помилка label_healthy: Справність label_pending: Очікування + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: з %{datetime} - subtitle: Папки проєкту з автоматичним керуванням title: Стан справності health_email_notifications: description_subscribed: Усі адміністратори отримують сповіщення електронною поштою про стан справності цього сховища. diff --git a/modules/storages/config/locales/crowdin/uz.yml b/modules/storages/config/locales/crowdin/uz.yml index 75bfaceec030..75d1d3bd93d8 100644 --- a/modules/storages/config/locales/crowdin/uz.yml +++ b/modules/storages/config/locales/crowdin/uz.yml @@ -102,11 +102,24 @@ uz: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/vi.yml b/modules/storages/config/locales/crowdin/vi.yml index d94a155278b3..401297fffb12 100644 --- a/modules/storages/config/locales/crowdin/vi.yml +++ b/modules/storages/config/locales/crowdin/vi.yml @@ -102,11 +102,24 @@ vi: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Lỗi label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/locales/crowdin/zh-CN.yml b/modules/storages/config/locales/crowdin/zh-CN.yml index 65684023de2c..78d313b80870 100644 --- a/modules/storages/config/locales/crowdin/zh-CN.yml +++ b/modules/storages/config/locales/crowdin/zh-CN.yml @@ -102,11 +102,24 @@ zh-CN: storage_provider: 存储提供商 health: checked: 上次检查于 %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: 错误 label_healthy: 健康 label_pending: 待处理 + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: 自从 %{datetime} - subtitle: 自动托管项目文件夹 title: 健康状态 health_email_notifications: description_subscribed: 所有管理员都会收到该存储的健康状态电子邮件通知。 diff --git a/modules/storages/config/locales/crowdin/zh-TW.yml b/modules/storages/config/locales/crowdin/zh-TW.yml index b05dfdff0a49..96da8a0f59a9 100644 --- a/modules/storages/config/locales/crowdin/zh-TW.yml +++ b/modules/storages/config/locales/crowdin/zh-TW.yml @@ -102,11 +102,24 @@ zh-TW: storage_provider: 儲存空間提供者 health: checked: 上次檢查時間:%{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: 錯誤 label_healthy: 健康的 label_pending: 待處理 + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: 自從%{datetime}開始 - subtitle: 自動管理的專案文件夾 title: 健康狀態 health_email_notifications: description_subscribed: 所有管理員都會收到此儲存空間的運作狀況電子郵件通知。 diff --git a/modules/storages/config/locales/en.yml b/modules/storages/config/locales/en.yml index 8e0acddddc4b..a127c89109ec 100644 --- a/modules/storages/config/locales/en.yml +++ b/modules/storages/config/locales/en.yml @@ -103,11 +103,24 @@ en: storage_provider: Storage provider health: checked: Last checked %{datetime} + connection_validation: + action: Recheck connection + client_id_wrong: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_wrong: The configured OAuth 2 client secret is invalid. Please check the configuration. + drive_id_wrong: The configured drive id could not be found. Please check the configuration. + not_configured: The connection could not be validated. Please finish configuration first. + placeholder: Check your connection against the server. + subtitle: Connection validation + tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + unexpected_content: Unexpected content found in the drive. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error label_healthy: Healthy label_pending: Pending + label_warning: Warning + project_folders: + subtitle: Automatically managed project folders since: since %{datetime} - subtitle: Automatically managed project folders title: Health status health_email_notifications: description_subscribed: All administrators receive health status email notifications for this storage. diff --git a/modules/storages/config/routes.rb b/modules/storages/config/routes.rb index 30617b2ca52b..04b2d533977b 100644 --- a/modules/storages/config/routes.rb +++ b/modules/storages/config/routes.rb @@ -38,11 +38,18 @@ post :finish_setup end - resource :automatically_managed_project_folders, controller: "/storages/admin/automatically_managed_project_folders", - only: %i[new create edit update] + resource :automatically_managed_project_folders, + controller: "/storages/admin/automatically_managed_project_folders", + only: %i[new create edit update] resource :access_management, controller: "/storages/admin/access_management", only: %i[new create edit update] + resource :connection_validation, + controller: "/storages/admin/connection_validation", + only: [] do + post :validate_connection, on: :member + end + get :select_provider, on: :collection member do diff --git a/modules/storages/lib/open_project/storages/engine.rb b/modules/storages/lib/open_project/storages/engine.rb index f40983616a25..a0115a9ffa4f 100644 --- a/modules/storages/lib/open_project/storages/engine.rb +++ b/modules/storages/lib/open_project/storages/engine.rb @@ -55,14 +55,23 @@ def self.permissions [ OpenProject::Events::MEMBER_CREATED, OpenProject::Events::MEMBER_UPDATED, - OpenProject::Events::MEMBER_DESTROYED, - OpenProject::Events::PROJECT_UPDATED, - OpenProject::Events::PROJECT_RENAMED, - OpenProject::Events::PROJECT_ARCHIVED, - OpenProject::Events::PROJECT_UNARCHIVED + OpenProject::Events::MEMBER_DESTROYED ].each do |event| - OpenProject::Notifications.subscribe(event) do |_payload| - ::Storages::ManageStorageIntegrationsJob.debounce + OpenProject::Notifications.subscribe(event) do |payload| + ::Storages::Storage.in_project(payload[:member].project_id).find_each do |storage| + ::Storages::AutomaticallyManagedStorageSyncJob.debounce(storage) + end + end + end + + [OpenProject::Events::PROJECT_UPDATED, + OpenProject::Events::PROJECT_RENAMED, + OpenProject::Events::PROJECT_ARCHIVED, + OpenProject::Events::PROJECT_UNARCHIVED].each do |event| + OpenProject::Notifications.subscribe(event) do |payload| + ::Storages::Storage.in_project(payload[:project].id).find_each do |storage| + ::Storages::AutomaticallyManagedStorageSyncJob.debounce(storage) + end end end @@ -97,7 +106,7 @@ def self.permissions ].each do |event| OpenProject::Notifications.subscribe(event) do |payload| if payload[:project_folder_mode] == :automatic - ::Storages::ManageStorageIntegrationsJob.debounce + ::Storages::AutomaticallyManagedStorageSyncJob.debounce(payload[:storage]) ::Storages::ManageStorageIntegrationsJob.disable_cron_job_if_needed end end @@ -106,13 +115,13 @@ def self.permissions OpenProject::Notifications.subscribe( ::OpenProject::Events::STORAGE_TURNED_UNHEALTHY ) do |payload| - Storages::HealthService.new(storage: payload[:storage]).unhealthy(reason: payload[:reason]) + ::Storages::HealthService.new(storage: payload[:storage]).unhealthy(reason: payload[:reason]) end OpenProject::Notifications.subscribe( ::OpenProject::Events::STORAGE_TURNED_HEALTHY ) do |payload| - Storages::HealthService.new(storage: payload[:storage]).healthy + ::Storages::HealthService.new(storage: payload[:storage]).healthy end end end @@ -239,7 +248,7 @@ def self.permissions exclude filter end - ::Queries::Register.register(::Queries::Projects::ProjectQuery) do + ::Queries::Register.register(::ProjectQuery) do filter ::Queries::Storages::Projects::Filter::StorageIdFilter filter ::Queries::Storages::Projects::Filter::StorageUrlFilter end diff --git a/modules/storages/spec/common/storages/peripherals/one_drive_connection_validator_spec.rb b/modules/storages/spec/common/storages/peripherals/one_drive_connection_validator_spec.rb new file mode 100644 index 000000000000..6aed3e2cf694 --- /dev/null +++ b/modules/storages/spec/common/storages/peripherals/one_drive_connection_validator_spec.rb @@ -0,0 +1,172 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require_module_spec_helper + +RSpec.describe Storages::Peripherals::OneDriveConnectionValidator do + let(:storage) { create(:one_drive_storage, oauth_client: create(:oauth_client)) } + + before do + Storages::Peripherals::Registry.stub("#{storage.short_provider_type}.queries.files", ->(_) { response }) + end + + subject { described_class.new(storage:).validate } + + context "if storage is not yet configured" do + let(:storage) { create(:one_drive_storage) } + + it "returns a validation failure" do + expect(subject.type).to eq(:none) + expect(subject.error_code).to eq(:wrn_not_configured) + expect(subject.description).to eq("The connection could not be validated. Please finish configuration first.") + end + end + + context "if the storage's tenant id could not be found" do + let(:error_payload) do + { + error: "invalid_request", + error_description: "There is an error. Tenant '#{storage.tenant_id}' not found. This is VERY bad." + }.to_json + end + let(:response) { build_failure(code: :unauthorized, payload: error_payload) } + + it "returns a validation failure" do + expect(subject.type).to eq(:error) + expect(subject.error_code).to eq(:err_tenant_invalid) + expect(subject.description) + .to eq("The configured directory (tenant) id is invalid. Please check the configuration.") + end + end + + context "if the storage's client id could not be found" do + let(:error_payload) { { error: "unauthorized_client" }.to_json } + let(:response) { build_failure(code: :unauthorized, payload: error_payload) } + + it "returns a validation failure" do + expect(subject.type).to eq(:error) + expect(subject.error_code).to eq(:err_client_invalid) + expect(subject.description).to eq("The configured OAuth 2 client id is invalid. Please check the configuration.") + end + end + + context "if the storage's client secret is wrong" do + let(:error_payload) { { error: "invalid_client" }.to_json } + let(:response) { build_failure(code: :unauthorized, payload: error_payload) } + + it "returns a validation failure" do + expect(subject.type).to eq(:error) + expect(subject.error_code).to eq(:err_client_invalid) + expect(subject.description) + .to eq("The configured OAuth 2 client secret is invalid. Please check the configuration.") + end + end + + context "if the storage's drive id could not be found" do + let(:response) { build_failure(code: :not_found, payload: nil) } + + it "returns a validation failure" do + expect(subject.type).to eq(:error) + expect(subject.error_code).to eq(:err_drive_invalid) + expect(subject.description).to eq("The configured drive id could not be found. Please check the configuration.") + end + end + + context "if the request fails with an unknown error" do + let(:response) { build_failure(code: :error, payload: nil) } + + before do + allow(Rails.logger).to receive(:error) + end + + it "returns a validation failure" do + expect(subject.type).to eq(:error) + expect(subject.error_code).to eq(:err_unknown) + expect(subject.description) + .to eq("The connection could not be validated. An unknown error occurred. " \ + "Please check the server logs for further information.") + end + + it "logs the error message" do + described_class.new(storage:).validate + expect(Rails.logger).to have_received(:error) + end + end + + context "if the request returns unexpected files" do + let(:storage) { create(:one_drive_storage, :as_automatically_managed, oauth_client: create(:oauth_client)) } + let(:project_folder_id) { "1337" } + let(:project_storage) do + create(:project_storage, + :as_automatically_managed, + project_folder_id:, + storage:, + project: create(:project)) + end + let(:files_result) do + Storages::StorageFiles.new( + [ + Storages::StorageFile.new(id: project_folder_id, name: "I am your father"), + Storages::StorageFile.new(id: "noooooooooo", name: "testimony_of_luke_skywalker.md") + ], + Storages::StorageFile.new(id: "root", name: "root"), + [] + ) + end + let(:response) { ServiceResult.success(result: files_result) } + + before do + project_storage + end + + it "returns a validation failure" do + expect(subject.type).to eq(:warning) + expect(subject.error_code).to eq(:wrn_unexpected_content) + expect(subject.description).to eq("Unexpected content found in the drive.") + end + end + + context "if everything was fine" do + let(:response) { ServiceResult.success } + + it "returns a validation success" do + expect(subject.type).to eq(:healthy) + expect(subject.error_code).to eq(:none) + expect(subject.description).to be_nil + end + end + + private + + def build_failure(code:, payload:) + data = Storages::StorageErrorData.new(source: "query", payload:) + error = Storages::StorageError.new(code:, data:) + ServiceResult.failure(result: code, errors: error) + end +end diff --git a/modules/storages/spec/common/storages/peripherals/registry_spec.rb b/modules/storages/spec/common/storages/peripherals/registry_spec.rb index 1c88d9aa85f4..340607402d38 100644 --- a/modules/storages/spec/common/storages/peripherals/registry_spec.rb +++ b/modules/storages/spec/common/storages/peripherals/registry_spec.rb @@ -508,26 +508,6 @@ end end - describe "#rename_file_command" do - before do - stub_request(:move, "https://example.com/remote.php/dav/files/OpenProject/OpenProject/asd") - .with( - headers: { - "Authorization" => "Basic T3BlblByb2plY3Q6T3BlblByb2plY3RTZWN1cmVQYXNzd29yZA==", - "Destination" => "/remote.php/dav/files/OpenProject/OpenProject/qwe" - } - ).to_return(status: 201, body: "", headers: {}) - end - - describe "with Nextcloud storage type selected" do - it "moves the file" do - result = registry.resolve("nextcloud.commands.rename_file").call(storage:, source: "OpenProject/asd", - target: "OpenProject/qwe") - expect(result).to be_success - end - end - end - describe "#delete_folder_command" do let(:auth_strategy) { Storages::Peripherals::StorageInteraction::AuthenticationStrategies::BasicAuth.strategy } diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/rename_file_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/rename_file_command_spec.rb new file mode 100644 index 000000000000..ba39495963d6 --- /dev/null +++ b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/rename_file_command_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require_module_spec_helper + +RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::RenameFileCommand, :webmock do + let(:user) { create(:user) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) + end + let(:auth_strategy) { Storages::Peripherals::Registry.resolve("nextcloud.authentication.userbound").call(user:) } + + it_behaves_like "rename_file_command: basic command setup" + + it_behaves_like "rename_file_command: validating input data" + + context "when renaming a folder", vcr: "nextcloud/rename_file_success" do + let(:file_id) { "169" } + let(:name) { "I am the senat" } + + it_behaves_like "rename_file_command: successful file renaming" + end + + context "when renaming a file inside a subdirectory", vcr: "nextcloud/rename_file_with_location_success" do + let(:file_id) { "167" } + let(:name) { "I❤️you death star.md" } + + it_behaves_like "rename_file_command: successful file renaming" + end + + context "when trying to rename a not existent file", vcr: "nextcloud/rename_file_not_found" do + let(:file_id) { "sith_have_yellow_light_sabers" } + let(:name) { "this_will_not_happen.txt" } + let(:error_source) { Storages::Peripherals::StorageInteraction::Nextcloud::FileInfoQuery } + + it_behaves_like "rename_file_command: not found" + end +end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/create_folder_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/create_folder_command_spec.rb index d96b8e27b2ad..b145abf6d06f 100644 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/create_folder_command_spec.rb +++ b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/create_folder_command_spec.rb @@ -42,7 +42,7 @@ context "when creating a folder in the root", vcr: "one_drive/create_folder_root" do let(:folder_name) { "Földer CreatedBy Çommand" } let(:parent_location) { Storages::Peripherals::ParentFolder.new("/") } - let(:path) { "/#{folder_name}" } + let(:path) { "/F%C3%B6lder%20CreatedBy%20%C3%87ommand" } it_behaves_like "create_folder_command: successful folder creation" end @@ -50,7 +50,7 @@ context "when creating a folder in a parent folder", vcr: "one_drive/create_folder_parent" do let(:folder_name) { "Földer CreatedBy Çommand" } let(:parent_location) { Storages::Peripherals::ParentFolder.new("01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU") } - let(:path) { "/Folder with spaces/#{folder_name}" } + let(:path) { "/Folder%20with%20spaces/F%C3%B6lder%20CreatedBy%20%C3%87ommand" } it_behaves_like "create_folder_command: successful folder creation" end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/rename_file_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/rename_file_command_spec.rb index 8552a2bbb683..1b498d4ea11f 100644 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/rename_file_command_spec.rb +++ b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/rename_file_command_spec.rb @@ -33,46 +33,31 @@ RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::RenameFileCommand, :webmock do let(:storage) { create(:sharepoint_dev_drive_storage) } - let(:userless_strategy) { Storages::Peripherals::Registry.resolve("one_drive.authentication.userless").call } - let(:folder) do - Storages::Peripherals::Registry - .resolve("one_drive.commands.create_folder") - .call(auth_strategy: userless_strategy, storage:, folder_name: "Wrong Name", - parent_location: Storages::Peripherals::ParentFolder.new("/")) - end + let(:auth_strategy) { Storages::Peripherals::Registry.resolve("one_drive.authentication.userless").call } - subject(:command) { described_class.new(storage) } + it_behaves_like "rename_file_command: basic command setup" - it "is registered as rename_file" do - expect(Storages::Peripherals::Registry.resolve("one_drive.commands.rename_file")).to eq(described_class) - end + it_behaves_like "rename_file_command: validating input data" - it "responds to .call with correct parameters" do - expect(described_class).to respond_to(:call) + context "when renaming a folder", vcr: "one_drive/rename_file_success" do + let(:file_id) { "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR" } + let(:name) { "I am the senat" } - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq source], %i[keyreq target]) + it_behaves_like "rename_file_command: successful file renaming" end - it "renames a folder", vcr: "one_drive/rename_folder_success" do - file_info = folder.result - - result = command.call(source: file_info.id, target: "My Project No. 1 (19)") - - expect(result).to be_success - renamed_details = result.result + context "when renaming a file inside a subdirectory", vcr: "one_drive/rename_file_with_location_success" do + let(:file_id) { "01AZJL5PPMSBBO3R2BIZHJFCELSW3RP7GN" } + let(:name) { "I❤️you death star.png" } - expect(renamed_details.name).to eq("My Project No. 1 (19)") - expect(renamed_details.id).to eq(file_info.id) - ensure - delete_folder(folder.result.id) + it_behaves_like "rename_file_command: successful file renaming" end - private + context "when trying to rename a not existent file", vcr: "one_drive/rename_file_not_found" do + let(:file_id) { "sith_have_yellow_light_sabers" } + let(:name) { "this_will_not_happen.png" } + let(:error_source) { described_class } - def delete_folder(folder_id) - Storages::Peripherals::Registry - .resolve("one_drive.commands.delete_folder") - .call(auth_strategy: userless_strategy, storage:, location: folder_id) + it_behaves_like "rename_file_command: not found" end end 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 ff0e9b2af7ff..21a69db16310 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 @@ -33,7 +33,7 @@ # Test if the deletion of a ProjectStorage actually deletes related FileLink # objects. -RSpec.describe "Delete ProjectStorage with FileLinks", :js, :with_cuprite, :webmock do +RSpec.describe "Delete ProjectStorage with FileLinks", :js, :webmock, :with_cuprite do let(:user) { create(:user) } let(:role) { create(:project_role, permissions: [:manage_files_in_project]) } let(:project) do @@ -69,7 +69,7 @@ visit external_file_storages_project_settings_project_storages_path(project) # The list of enabled file storages should now contain Storage 1 - expect(page).to have_selector('h1', text: 'Files') + expect(page).to have_css("h1", text: "Files") expect(page).to have_text("Storage 1") # Press Delete icon to remove the storage from the project diff --git a/modules/storages/spec/features/hide_attachments_spec.rb b/modules/storages/spec/features/hide_attachments_spec.rb index 1f1ac0b4d302..9a96d5193974 100644 --- a/modules/storages/spec/features/hide_attachments_spec.rb +++ b/modules/storages/spec/features/hide_attachments_spec.rb @@ -74,7 +74,7 @@ context "if Setting.show_work_package_attachments is false", with_settings: { show_work_package_attachments: false } do let(:project) { create(:project) } - it 'renders the toggle as off for project with not set deactivate_work_package_attachments' do + it "renders the toggle as off for project with not set deactivate_work_package_attachments" do expect(project.deactivate_work_package_attachments).to be_nil login_as current_user @@ -89,7 +89,7 @@ context "if Setting.show_work_package_attachments is true", with_settings: { show_work_package_attachments: true } do let(:project) { create(:project) } - it 'renders the toggle as on for project with not set deactivate_work_package_attachments' do + it "renders the toggle as on for project with not set deactivate_work_package_attachments" do expect(project.deactivate_work_package_attachments).to be_nil login_as current_user diff --git a/modules/storages/spec/features/manage_project_storage_spec.rb b/modules/storages/spec/features/manage_project_storage_spec.rb index 758055dd9f03..df11ab2e0708 100644 --- a/modules/storages/spec/features/manage_project_storage_spec.rb +++ b/modules/storages/spec/features/manage_project_storage_spec.rb @@ -38,10 +38,7 @@ # We decrease the notification polling interval because some portions of the JS code rely on something triggering # the Angular change detection. This is usually done by the notification polling, but we don't want to wait RSpec.describe("Activation of storages in projects", - :js, - :with_cuprite, - :webmock, - with_settings: { notifications_polling_interval: 1_000 }) do + :js, :webmock, :with_cuprite, with_settings: { notifications_polling_interval: 1_000 }) do let(:user) { create(:user) } # The first page is the Project -> Settings -> General page, so we need # to provide the user with the edit_project permission in the role. @@ -112,7 +109,7 @@ expect(page).to have_title("Files") expect(page).to have_current_path external_file_storages_project_settings_project_storages_path(project) expect(page).to have_text(I18n.t("storages.no_results")) - page.first(:link, 'New storage').click + page.first(:link, "New storage").click # Can cancel the creation of a new file storage expect(page).to have_current_path new_project_settings_project_storage_path(project_id: project) @@ -121,7 +118,7 @@ expect(page).to have_current_path external_file_storages_project_settings_project_storages_path(project) # Enable one file storage together with a project folder mode - page.first(:link, 'New storage').click + page.first(:link, "New storage").click 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", @@ -149,14 +146,14 @@ page.click_button("Add") # The list of enabled file storages should now contain Storage 1 - expect(page).to have_selector('h1', text: 'Files') + expect(page).to have_css("h1", text: "Files") 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_project_storage_path(project_id: project, id: Storages::ProjectStorage.last, - storages_project_storage: {project_folder_mode: "manual"}) + storages_project_storage: { project_folder_mode: "manual" }) expect(page).to have_text("Edit the file storage to this project") expect(page).to have_no_select("storages_project_storage_storage_id") expect(page).to have_text(storage.name) @@ -169,14 +166,14 @@ page.click_button("Save") # The list of enabled file storages should still contain Storage 1 - expect(page).to have_selector('h1', text: 'Files') + expect(page).to have_css("h1", text: "Files") 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_project_storage_path(project_id: project, id: Storages::ProjectStorage.last, - storages_project_storage: {project_folder_mode: "inactive"}) + storages_project_storage: { project_folder_mode: "inactive" }) expect(page).to have_text("Edit the file storage to this project") page.click_link("Cancel") expect(page).to have_current_path external_file_storages_project_settings_project_storages_path(project) @@ -237,7 +234,7 @@ it "excludes storages that are not configured correctly" do visit external_file_storages_project_settings_project_storages_path(project) - page.first(:link, 'New storage').click + page.first(:link, "New storage").click aggregate_failures "select field options" do expect(page).to have_select("storages_project_storage_storage_id", diff --git a/modules/storages/spec/features/storages/project_settings/oauth_access_grant_spec.rb b/modules/storages/spec/features/storages/project_settings/oauth_access_grant_spec.rb index a1af0089e163..55f6c9189529 100644 --- a/modules/storages/spec/features/storages/project_settings/oauth_access_grant_spec.rb +++ b/modules/storages/spec/features/storages/project_settings/oauth_access_grant_spec.rb @@ -75,7 +75,7 @@ expect(page).to have_checked_field("New folder with automatically managed permissions") click_on("Add") - expect(page).to have_selector('h1', text: 'Files') + expect(page).to have_css("h1", text: "Files") expect(page).to have_text(storage.name) within_test_selector("oauth-access-grant-nudge-modal") do diff --git a/modules/storages/spec/requests/api/v3/file_links/file_links_api_spec.rb b/modules/storages/spec/requests/api/v3/file_links/file_links_api_spec.rb index 1fa11b3698b7..7b1ad6e7cd6e 100644 --- a/modules/storages/spec/requests/api/v3/file_links/file_links_api_spec.rb +++ b/modules/storages/spec/requests/api/v3/file_links/file_links_api_spec.rb @@ -247,7 +247,7 @@ def disable_module(project, modul) let(:path) { "#{api_v3_paths.file_links(work_package.id)}?filters=#{CGI.escape(filters.to_json)}" } it "return a 400 HTTP error" do - expect(last_response.status).to be 400 + expect(last_response).to have_http_status :bad_request end end end @@ -636,7 +636,7 @@ def disable_module(project, modul) let(:error) { :not_found } it "fails with outbound request failure" do - expect(last_response.status).to be(500) + expect(last_response).to have_http_status(:internal_server_error) body = JSON.parse(last_response.body) expect(body["message"]).to eq(I18n.t("api_v3.errors.code_500_outbound_request_failure", status_code: 404)) diff --git a/modules/storages/spec/requests/api/v3/storages/storage_files_api_spec.rb b/modules/storages/spec/requests/api/v3/storages/storage_files_api_spec.rb index ec601f9c5703..f317c0f07668 100644 --- a/modules/storages/spec/requests/api/v3/storages/storage_files_api_spec.rb +++ b/modules/storages/spec/requests/api/v3/storages/storage_files_api_spec.rb @@ -31,7 +31,6 @@ require "spec_helper" require_module_spec_helper -# rubocop:disable RSpecRails/HaveHttpStatus RSpec.describe "API v3 storage files", :webmock, content_type: :json do include API::V3::Utilities::PathHelper include StorageServerHelpers @@ -137,20 +136,20 @@ context "with authorization failure" do let(:error) { :unauthorized } - it { expect(last_response.status).to be(500) } + it { expect(last_response).to have_http_status(:internal_server_error) } end context "with internal error" do let(:error) { :error } - it { expect(last_response.status).to be(500) } + it { expect(last_response).to have_http_status(:internal_server_error) } end context "with not found" do let(:error) { :not_found } it "fails with outbound request failure" do - expect(last_response.status).to be(500) + expect(last_response).to have_http_status(:internal_server_error) body = JSON.parse(last_response.body) expect(body["message"]).to eq(I18n.t("api_v3.errors.code_500_outbound_request_failure", status_code: 404)) @@ -218,7 +217,7 @@ let(:error) { :forbidden } it "fails with outbound request failure" do - expect(last_response.status).to be(500) + expect(last_response).to have_http_status(:internal_server_error) body = JSON.parse(last_response.body) expect(body["message"]).to eq(I18n.t("api_v3.errors.code_500_outbound_request_failure", status_code: 403)) @@ -229,14 +228,14 @@ context "with internal error" do let(:error) { :error } - it { expect(last_response.status).to be(500) } + it { expect(last_response).to have_http_status(:internal_server_error) } end context "with not found" do let(:error) { :not_found } it "fails with outbound request failure" do - expect(last_response.status).to be(500) + expect(last_response).to have_http_status(:internal_server_error) body = JSON.parse(last_response.body) expect(body["message"]).to eq(I18n.t("api_v3.errors.code_500_outbound_request_failure", status_code: 404)) @@ -288,20 +287,20 @@ describe "due to authorization failure" do let(:error) { :unauthorized } - it { expect(last_response.status).to be(500) } + it { expect(last_response).to have_http_status(:internal_server_error) } end describe "due to internal error" do let(:error) { :error } - it { expect(last_response.status).to be(500) } + it { expect(last_response).to have_http_status(:internal_server_error) } end describe "due to not found" do let(:error) { :not_found } it "fails with outbound request failure" do - expect(last_response.status).to be(500) + expect(last_response).to have_http_status(:internal_server_error) body = JSON.parse(last_response.body) expect(body["message"]).to eq(I18n.t("api_v3.errors.code_500_outbound_request_failure", status_code: 404)) @@ -313,15 +312,13 @@ context "with invalid request body" do let(:body) { { fileNam_: "ape.png", parent: "/Pictures", projectId: project.id }.to_json } - it { expect(last_response.status).to be(400) } + it { expect(last_response).to have_http_status(:bad_request) } end context "without ee token", with_ee: false do let(:storage) { create(:one_drive_storage, creator: current_user) } - it { expect(last_response.status).to be(500) } + it { expect(last_response).to have_http_status(:internal_server_error) } end end end - -# rubocop:enable RSpecRails/HaveHttpStatus diff --git a/modules/storages/spec/requests/project_storages_open_spec.rb b/modules/storages/spec/requests/project_storages_open_spec.rb index 9db041b8c55e..d5f61a9cc25a 100644 --- a/modules/storages/spec/requests/project_storages_open_spec.rb +++ b/modules/storages/spec/requests/project_storages_open_spec.rb @@ -65,7 +65,7 @@ it "redirects to api_v3_projects_storage_open_url" do get route, {}, { "HTTP_ACCEPT" => "text/html" } - expect(last_response.status).to eq (302) + expect(last_response).to have_http_status(:found) expect(last_response.headers["Location"]).to eq(expected_redirect_url) end end @@ -74,7 +74,7 @@ it "renders an appropirate turbo_stream" do get route, {}, { "HTTP_ACCEPT" => "text/vnd.turbo-stream.html" } - expect(last_response.status).to eq (200) + expect(last_response).to have_http_status(:ok) expect(last_response.body).to eq ("\n \n\n\n") end end @@ -99,7 +99,7 @@ it "redirects to ensure_connection url with current request url as a destination_url" do get route, {}, { "HTTP_ACCEPT" => "text/html" } - expect(last_response.status).to eq (302) + expect(last_response).to have_http_status(:found) expect(last_response.headers["Location"]).to eq ( "http://example.org/oauth_clients/#{storage.oauth_client.client_id}/ensure_connection?destination_url=http%3A%2F%2Fexample.org%2Fprojects%2F#{project.identifier}%2Fproject_storages%2F#{project_storage.id}%2Fopen&storage_id=#{storage.id}" ) @@ -110,7 +110,7 @@ it "redirects to project overview page with modal flash set up" do get route, {}, { "HTTP_ACCEPT" => "text/html" } - expect(last_response.status).to eq (302) + expect(last_response).to have_http_status(:found) expect(last_response.headers["Location"]).to eq ("http://example.org/projects/#{project.identifier}") expect(last_request.session["flash"]["flashes"]) .to eq({ @@ -129,7 +129,7 @@ it "responds with 204 no content" do get route, {}, { "HTTP_ACCEPT" => "text/vnd.turbo-stream.html" } - expect(last_response.status).to eq (204) + expect(last_response).to have_http_status(:no_content) expect(last_response.body).to eq ("") end end @@ -141,7 +141,7 @@ it "redirects to project overview page with modal flash set up" do get route, {}, { "HTTP_ACCEPT" => "text/html" } - expect(last_response.status).to eq (302) + expect(last_response).to have_http_status(:found) expect(last_response.headers["Location"]).to eq ("http://example.org/projects/#{project.identifier}") expect(last_request.session["flash"]["flashes"]) .to eq({ @@ -159,7 +159,7 @@ it "responds with 204 no content" do get route, {}, { "HTTP_ACCEPT" => "text/vnd.turbo-stream.html" } - expect(last_response.status).to eq (204) + expect(last_response).to have_http_status(:no_content) expect(last_response.body).to eq ("") end end @@ -170,7 +170,7 @@ it "redirects to storage_open_url" do get route, {}, { "HTTP_ACCEPT" => "text/html" } - expect(last_response.status).to eq (302) + expect(last_response).to have_http_status(:found) expect(last_response.headers["Location"]).to eq (expected_redirect_url) end end @@ -181,7 +181,7 @@ it "responds with 403" do get route, {}, { "HTTP_ACCEPT" => "text/html" } - expect(last_response.status).to eq(403) + expect(last_response).to have_http_status(:forbidden) end end end @@ -189,7 +189,7 @@ context "when user is not logged in" do it "responds with 401" do get route - expect(last_response.status).to eq(401) + expect(last_response).to have_http_status(:unauthorized) end end end diff --git a/modules/storages/spec/requests/storages/admin/connection_validation_spec.rb b/modules/storages/spec/requests/storages/admin/connection_validation_spec.rb new file mode 100644 index 000000000000..b3b0cfe7b9de --- /dev/null +++ b/modules/storages/spec/requests/storages/admin/connection_validation_spec.rb @@ -0,0 +1,142 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" + +RSpec.describe "connection validation", :skip_csrf do + describe "POST /admin/settings/storages/:id/connection_validation/validate_connection" do + let(:storage) { create(:one_drive_storage) } + let(:user) { create(:admin) } + let(:validator) do + double = instance_double(Storages::Peripherals::OneDriveConnectionValidator) + allow(double).to receive_messages(validate: validation_result) + double + end + + current_user { user } + + before do + allow(Storages::Peripherals::OneDriveConnectionValidator).to receive(:new).and_return(validator) + end + + subject do + post validate_connection_admin_settings_storage_connection_validation_path(storage.id, format: :turbo_stream) + end + + shared_examples_for "a validation result template" do |show_timestamp:, label:, description:| + it "returns a turbo update template" do + expect(subject.status).to eq(200) + + doc = Nokogiri::HTML(subject.body) + expect(doc.xpath(xpath_for_subtitle).text).to eq("Connection validation") + + if show_timestamp + expect(doc.xpath(xpath_for_timestamp)).not_to be_empty + else + expect(doc.xpath(xpath_for_timestamp)).to be_empty + end + + if label.present? + expect(doc.xpath(xpath_for_label).text).to eq(label) + else + expect(doc.xpath(xpath_for_label).text).to be_empty + end + + if description.present? + expect(doc.xpath(xpath_for_description).text).to eq(description) + else + expect(doc.xpath(xpath_for_description).text).to be_empty + end + end + end + + context "if the a validation result of type :none (no validation executed) is returned" do + let(:validation_result) do + Storages::ConnectionValidation.new(type: :none, + error_code: :none, + timestamp: Time.current, + description: "not configured") + end + + it_behaves_like "a validation result template", show_timestamp: false, label: nil, description: "not configured" + end + + context "if validator returns an error" do + let(:validation_result) do + Storages::ConnectionValidation.new(type: :error, + error_code: :my_err, + timestamp: Time.current, + description: "An error occurred") + end + + it_behaves_like "a validation result template", + show_timestamp: true, label: "Error", description: "MY_ERR: An error occurred" + end + + context "if validator returns a warning" do + let(:validation_result) do + Storages::ConnectionValidation.new(type: :warning, + error_code: :my_wrn, + timestamp: Time.current, + description: "There is something weird...") + end + + it_behaves_like "a validation result template", + show_timestamp: true, label: "Warning", description: "MY_WRN: There is something weird..." + end + + context "if validator returns a success" do + let(:validation_result) do + Storages::ConnectionValidation.new(type: :healthy, error_code: :none, timestamp: Time.current, description: nil) + end + + it_behaves_like "a validation result template", + show_timestamp: true, label: "Healthy", description: nil + end + end + + private + + def xpath_for_subtitle + "#{xpath_for_turbo_target}/div/div/span[@data-test-selector='validation-result--subtitle']" + end + + def xpath_for_timestamp + "#{xpath_for_turbo_target}/div/div/span[@data-test-selector='validation-result--timestamp']" + end + + def xpath_for_label + "#{xpath_for_turbo_target}/div/div/span[contains(@class, 'Label')]" + end + + def xpath_for_description + "#{xpath_for_turbo_target}/div/div/span[@data-test-selector='validation-result--description']" + end + + def xpath_for_turbo_target = "//turbo-stream[@target='storages-admin-sidebar-validation-result-component']/template" +end diff --git a/modules/storages/spec/requests/storages/project_settings/oauth_access_grant_flow_spec.rb b/modules/storages/spec/requests/storages/project_settings/oauth_access_grant_flow_spec.rb index f925e9a49a11..eb2def019f97 100644 --- a/modules/storages/spec/requests/storages/project_settings/oauth_access_grant_flow_spec.rb +++ b/modules/storages/spec/requests/storages/project_settings/oauth_access_grant_flow_spec.rb @@ -55,7 +55,7 @@ project_id: project_storage.project.id, id: project_storage ) - expect(last_response.status).to eq(401) + expect(last_response).to have_http_status(:unauthorized) end end @@ -78,7 +78,7 @@ project_id: project_storage.project.id, id: project_storage ) - expect(last_response.status).to eq(302) + expect(last_response).to have_http_status(:found) expect(last_response.location).to eq( "#{storage.host}/index.php/apps/oauth2/authorize?client_id=#{storage.oauth_client.client_id}&" \ "redirect_uri=#{redirect_uri}&response_type=code&state=#{nonce}" @@ -104,7 +104,7 @@ ) storage.oauth_client - expect(last_response.status).to eq(302) + expect(last_response).to have_http_status(:found) expect(last_response.location).to eq("http://example.org/projects/#{project.id}/settings/project_storages/external_file_storages") expect(last_response.cookies.keys).to eq(["_open_project_session"]) end diff --git a/modules/storages/spec/services/storages/nextcloud_group_folder_properties_sync_service_spec.rb b/modules/storages/spec/services/storages/nextcloud_group_folder_properties_sync_service_spec.rb index 9f1f0178c653..9bf3bde81e1b 100644 --- a/modules/storages/spec/services/storages/nextcloud_group_folder_properties_sync_service_spec.rb +++ b/modules/storages/spec/services/storages/nextcloud_group_folder_properties_sync_service_spec.rb @@ -534,6 +534,22 @@ XML end + let(:get_file_info_response_body) do + { + ocs: { + data: { + status: "OK", + statuscode: 200, + id: project_storage2.project_folder_id, + name: "Lost Jedi Project Folder #3", + mtime: 1691079621, + ctime: 0, + dav_permissions: "RMGDNVW", + path: "files/OpenProject/Lost Jedi Project Folder #3" + } + } + }.to_json + end let(:request_stubs) { [] } @@ -644,7 +660,7 @@ expect(project_storage2.last_project_folders.pluck(:origin_folder_id)).to eq(["123"]) expect(project_storage3.last_project_folders.pluck(:origin_folder_id)).to eq(["2600003"]) - expect(request_stubs).to all have_been_requested + expect_all_stubs end describe "error handling and flow control" do @@ -759,9 +775,12 @@ it "continues normally ignoring that folder" do expect { described_class.new(storage).call }.not_to change(project_storage1, :project_folder_id) - expect(request_stubs.delete_at(5)).not_to have_been_requested - expect(request_stubs.delete_at(3)).not_to have_been_requested - expect(request_stubs).to all(have_been_requested) + expect(request_stubs[..2]).to all(have_been_requested) + expect(request_stubs[3]).not_to have_been_requested + expect(request_stubs[4]).to have_been_made.times(2) + expect(request_stubs[5]).to have_been_requested + expect(request_stubs[6]).not_to have_been_requested + expect(request_stubs[7..]).to all(have_been_requested) end it "logs the occurrence" do @@ -779,19 +798,19 @@ context "when renaming a folder fail" do before do - request_stubs[4] = stub_request(:move, + request_stubs[5] = stub_request(:move, "#{storage.host}/remote.php/dav/files/OpenProject/OpenProject/" \ - "Lost%20Jedi%20Project%20Folder%20%233/") + "Lost%20Jedi%20Project%20Folder%20%233") .with(headers: { "Authorization" => "Basic T3BlblByb2plY3Q6MTIzNDU2Nzg=", "Destination" => "/remote.php/dav/files/OpenProject/OpenProject/" \ - "Jedi%20Project%20Folder%20%7C%7C%7C%20%28#{project2.id}%29/" }) + "Jedi%20Project%20Folder%20%7C%7C%7C%20%28#{project2.id}%29" }) .to_return(status: 404, body: "", headers: {}) end it "we stop processing to avoid issues with permissions" do described_class.new(storage).call - request_stubs[5..].each { |request| expect(request).not_to have_been_requested } + request_stubs[6..].each { |request| expect(request).not_to have_been_requested } end it "logs the occurrence" do @@ -800,8 +819,8 @@ expect(OpenProject.logger) .to have_received(:warn) - .with(source: "OpenProject/Lost Jedi Project Folder #3/", - target: project_storage2.managed_project_folder_path, + .with(folder_id: project_storage2.project_folder_id, + folder_name: "Jedi Project Folder ||| (#{project2.id})", command: Storages::Peripherals::StorageInteraction::Nextcloud::RenameFileCommand, message: "Outbound request destination not found", data: { status: 404, body: "" }) @@ -823,7 +842,7 @@ it "does not interrupt the flow" do described_class.new(storage).call - expect(request_stubs).to all(have_been_requested) + expect_all_stubs end it "logs the occurrence" do @@ -855,7 +874,7 @@ it "does not interrupt the flow" do described_class.new(storage).call - expect(request_stubs).to all(have_been_requested) + expect_all_stubs end it "logs the occurrence" do @@ -886,7 +905,7 @@ it "does not interrupt te flow" do described_class.new(storage).call - expect(request_stubs).to all have_been_requested + expect_all_stubs end it "logs the occurrence" do @@ -921,7 +940,7 @@ it "does not interrupt the flow" do described_class.new(storage).call - expect(request_stubs).to all have_been_requested + expect_all_stubs end it "logs the occurrence and continues the flow" do @@ -993,19 +1012,30 @@ def setup_request_stubs body: created_folder_propfind_response_body, headers: { "Content-Type" => "application/xml" }) - # 4 - Move/Rename Folder + # 4 - Fetch folder information + request_stubs << stub_request( + :get, + "#{storage.host}/ocs/v1.php/apps/integration_openproject/fileinfo/#{project_storage2.project_folder_id}" + ).with( + headers: { + "Authorization" => "Basic T3BlblByb2plY3Q6MTIzNDU2Nzg=", + "OCS-APIRequest" => "true" + } + ).to_return(status: 200, body: get_file_info_response_body, headers: {}) + + # 5 - Move/Rename Folder request_stubs << stub_request( :move, - "#{storage.host}/remote.php/dav/files/OpenProject/OpenProject/Lost%20Jedi%20Project%20Folder%20%233/" + "#{storage.host}/remote.php/dav/files/OpenProject/OpenProject/Lost%20Jedi%20Project%20Folder%20%233" ).with( headers: { "Authorization" => "Basic T3BlblByb2plY3Q6MTIzNDU2Nzg=", "Destination" => "/remote.php/dav/files/OpenProject/OpenProject/" \ - "Jedi%20Project%20Folder%20%7C%7C%7C%20%28#{project2.id}%29/" + "Jedi%20Project%20Folder%20%7C%7C%7C%20%28#{project2.id}%29" } ).to_return(status: 201, body: "", headers: {}) - # 5 - Set Permissions for the Created Folder + # 6 - Set Permissions for the Created Folder request_stubs << stub_request( :proppatch, "#{storage.host}/remote.php/dav/files/OpenProject/OpenProject/" \ @@ -1019,7 +1049,7 @@ def setup_request_stubs body: created_folder_set_permissions_response_body, headers: { "Content-Type" => "application/xml" }) - # 6 - Hide Unknown Inactive Folder + # 7 - Hide Unknown Inactive Folder request_stubs << stub_request( :proppatch, "#{storage.host}/remote.php/dav/files/OpenProject/OpenProject/Lost%20Jedi%20Project%20Folder%20%232/" @@ -1032,7 +1062,7 @@ def setup_request_stubs body: hide_folder_set_permissions_response_body, headers: { "Content-Type" => "application/xml" }) - # 7 - Hide Inactive Project Folder + # 8 - Hide Inactive Project Folder request_stubs << stub_request( :proppatch, "#{storage.host}/remote.php/dav/files/OpenProject/OpenProject/NOT%20ACTIVE%20PROJECT/" @@ -1043,7 +1073,7 @@ def setup_request_stubs } ).to_return(status: 207, body: set_permissions_response_body5, headers: { "Content-Type" => "application/xml" }) - # 8 - Set folder Permissions + # 9 - Set folder Permissions request_stubs << stub_request( :proppatch, "#{storage.host}/remote.php/dav/files/OpenProject/OpenProject/" \ @@ -1055,7 +1085,7 @@ def setup_request_stubs } ).to_return(status: 207, body: set_permissions_response_body, headers: { "Content-Type" => "application/xml" }) - # 9 - Set public project folder permissions + # 10 - Set public project folder permissions request_stubs << stub_request( :proppatch, "#{storage.host}/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20%28#{project_public.id}%29/" @@ -1066,6 +1096,7 @@ def setup_request_stubs } ).to_return(status: 207, body: set_permissions_response_body6, headers: { "Content-Type" => "application/xml" }) + # 11 request_stubs << stub_request( :proppatch, "#{storage.host}/remote.php/dav/files/OpenProject/OpenProject/Project3%20%28#{project3.id}%29/" @@ -1076,7 +1107,7 @@ def setup_request_stubs } ).to_return(status: 207, body: set_permissions_response_body7, headers: { "Content-Type" => "application/xml" }) - # 11 - Get all user in the remote group + # 12 - Get all user in the remote group request_stubs << stub_request(:get, "#{storage.host}/ocs/v1.php/cloud/groups/#{storage.group}") .with( headers: { @@ -1087,7 +1118,7 @@ def setup_request_stubs body: group_users_response_body, headers: { "Content-Type" => "application/xml" }) - # 12 - Add user to group + # 13 - Add user to group request_stubs << stub_request(:post, "#{storage.host}/ocs/v1.php/cloud/users/Obi-Wan/groups") .with( body: "groupid=OpenProject", @@ -1133,6 +1164,12 @@ def setup_request_stubs ).to_return(status: 200, body: remove_user_from_group_response, headers: { "Content-Type" => "application/xml" }) end + def expect_all_stubs + expect(request_stubs[..3]).to all(have_been_requested) + expect(request_stubs[4]).to have_been_made.times(2) + expect(request_stubs[5..]).to all(have_been_requested) + end + def parse_error_msg(msg) MultiJson.load(msg, symbolize_keys: true) end diff --git a/modules/storages/spec/services/storages/one_drive_managed_folder_sync_service_spec.rb b/modules/storages/spec/services/storages/one_drive_managed_folder_sync_service_spec.rb index 2b39643c58f6..818b16c793a7 100644 --- a/modules/storages/spec/services/storages/one_drive_managed_folder_sync_service_spec.rb +++ b/modules/storages/spec/services/storages/one_drive_managed_folder_sync_service_spec.rb @@ -304,8 +304,8 @@ expect(OpenProject.logger) .to have_received(:warn) - .with(source: project_storage.project_folder_id, - target: "[Sample] Project Name _ Ehuu (#{project.id})", + .with(folder_id: project_storage.project_folder_id, + folder_name: "[Sample] Project Name _ Ehuu (#{project.id})", command: Storages::Peripherals::StorageInteraction::OneDrive::RenameFileCommand, message: nil, data: { status: :conflict, body: /nameAlreadyExists/ }) 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 index 7039fbf212ac..d32691286caa 100644 --- 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 @@ -41,7 +41,7 @@ subject expect(OpenProject::Notifications).to( - have_received(:send).with(event, project_folder_mode: mode) + have_received(:send).with(event, project_folder_mode: mode, storage: model_instance.storage) ) end end diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/rename_file_not_found.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/rename_file_not_found.yml new file mode 100644 index 000000000000..5166e1a939ca --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/rename_file_not_found.yml @@ -0,0 +1,82 @@ +--- +http_interactions: +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/sith_have_yellow_light_sabers + body: + encoding: US-ASCII + string: '' + headers: + Ocs-Apirequest: + - 'true' + Accept: + - application/json + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.2.6 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 24 Jun 2024 12:47:43 GMT + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Pragma: + - no-cache + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.59 (Debian) + Set-Cookie: + - oc07ul6b4oaw=b4791dd13f107ec63cbc8950b45bfe00; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=%2FdffEGtTYfogdGrFxXuCa6tHS0C5aJzklROR3cnYs4NDk%2Bji4cCdCTBvgE8Tke%2FumI3PnTJliuAO6h4yjL%2BHGpzr1ydwvQRdjCMx7vRj9tn7mX3JK%2B0PcTlCcfltUEzN; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=b4791dd13f107ec63cbc8950b45bfe00; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=b4791dd13f107ec63cbc8950b45bfe00; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=b4791dd13f107ec63cbc8950b45bfe00; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=b4791dd13f107ec63cbc8950b45bfe00; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=b4791dd13f107ec63cbc8950b45bfe00; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=b4791dd13f107ec63cbc8950b45bfe00; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=b4791dd13f107ec63cbc8950b45bfe00; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=b4791dd13f107ec63cbc8950b45bfe00; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.18 + X-Request-Id: + - 3H4NykbTwSA0lfCgR1V6 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '115' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"failure","statuscode":404,"message":"","totalitems":"","itemsperpage":""},"data":{"status":"Not + Found","statuscode":404}}}' + recorded_at: Mon, 24 Jun 2024 12:47:44 GMT +recorded_with: VCR 6.2.0 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/rename_file_success.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/rename_file_success.yml new file mode 100644 index 000000000000..171b2895028e --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/rename_file_success.yml @@ -0,0 +1,238 @@ +--- +http_interactions: +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/169 + body: + encoding: US-ASCII + string: '' + headers: + Ocs-Apirequest: + - 'true' + Accept: + - application/json + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.2.6 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 24 Jun 2024 12:47:43 GMT + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Pragma: + - no-cache + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.59 (Debian) + Set-Cookie: + - oc07ul6b4oaw=9549242a6c959c7a7d33cf2ba59948cf; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=SnqcEEH1biq%2FNMKJ7EcxDuhFcngpEBfV5QquyACM81e3l8yKW5e5tA7AcUwxkPF7V1lHoUuDGZhB8fvOntJDW%2Fj3nJHefyTWuTp0mIUgr3fzR5EzP5NVEUx5iyeq4iLt; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=9549242a6c959c7a7d33cf2ba59948cf; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=9549242a6c959c7a7d33cf2ba59948cf; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=9549242a6c959c7a7d33cf2ba59948cf; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=9549242a6c959c7a7d33cf2ba59948cf; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=9549242a6c959c7a7d33cf2ba59948cf; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=9549242a6c959c7a7d33cf2ba59948cf; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=9549242a6c959c7a7d33cf2ba59948cf; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=9549242a6c959c7a7d33cf2ba59948cf; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.18 + X-Request-Id: + - Zg8w9hGnNaix1M0Yl0Hr + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '254' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":169,"name":"Folder","mtime":1718624545,"ctime":0,"mimetype":"application\/x-op-directory","size":982733193,"owner_name":"admin","owner_id":"admin","modifier_name":null,"modifier_id":null,"dav_permissions":"RGDNVCK","path":"files\/Folder\/"}}}' + recorded_at: Mon, 24 Jun 2024 12:47:43 GMT +- request: + method: move + uri: https://nextcloud.local/remote.php/dav/files/admin/Folder + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + Destination: + - https://nextcloud.local/remote.php/dav/files/admin/I%20am%20the%20senat + User-Agent: + - httpx.rb/1.2.6 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Cache-Control: + - no-store, no-cache, must-revalidate + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Mon, 24 Jun 2024 12:47:43 GMT + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Oc-Fileid: + - '00000169oc07ul6b4oaw' + Pragma: + - no-cache + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.59 (Debian) + Set-Cookie: + - oc07ul6b4oaw=1fd3cbed011e6e0451e2e37195dfb126; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=AWx%2BB1wyQ4AIZRU7E%2BUlbQ3FjdBVzLPG0ECIy2tcc%2Fj9miXu9rdr5N%2Fs7C2XcqxsinTfK6E%2Fc%2BMZg2KeqduJd0YGVSZQFEevlYC52axc%2BH%2FeqTVmeAZhryuJcQGl6o%2F3; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=1fd3cbed011e6e0451e2e37195dfb126; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=1fd3cbed011e6e0451e2e37195dfb126; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=1fd3cbed011e6e0451e2e37195dfb126; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=1fd3cbed011e6e0451e2e37195dfb126; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=1fd3cbed011e6e0451e2e37195dfb126; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=1fd3cbed011e6e0451e2e37195dfb126; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=1fd3cbed011e6e0451e2e37195dfb126; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=1fd3cbed011e6e0451e2e37195dfb126; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - S7YtnR4rwLvCzI4xY2Tc + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.18 + X-Request-Id: + - S7YtnR4rwLvCzI4xY2Tc + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Mon, 24 Jun 2024 12:47:43 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/169 + body: + encoding: US-ASCII + string: '' + headers: + Ocs-Apirequest: + - 'true' + Accept: + - application/json + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.2.6 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 24 Jun 2024 12:47:43 GMT + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Pragma: + - no-cache + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.59 (Debian) + Set-Cookie: + - oc07ul6b4oaw=e924324c2778deea6e2d8fef29480bc2; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=pBVfixCW0LTylXE13Hp2p4Zd4P9SpWqpu7J2v9dol8cMWQQIqTrLrRhVgRshLwBJCzDTqD6xRLURz05smKCR4H9HuBnQWMqOy2J4GZm9Bx8Fo33v7MdYiivntbNDlwiB; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=e924324c2778deea6e2d8fef29480bc2; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=e924324c2778deea6e2d8fef29480bc2; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=e924324c2778deea6e2d8fef29480bc2; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=e924324c2778deea6e2d8fef29480bc2; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=e924324c2778deea6e2d8fef29480bc2; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=e924324c2778deea6e2d8fef29480bc2; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=e924324c2778deea6e2d8fef29480bc2; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=e924324c2778deea6e2d8fef29480bc2; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.18 + X-Request-Id: + - zyYddsBGCp2PoFAtvHfu + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '262' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":169,"name":"I + am the senat","mtime":1718624545,"ctime":0,"mimetype":"application\/x-op-directory","size":982733193,"owner_name":"admin","owner_id":"admin","modifier_name":null,"modifier_id":null,"dav_permissions":"RGDNVCK","path":"files\/I + am the senat\/"}}}' + recorded_at: Mon, 24 Jun 2024 12:47:43 GMT +recorded_with: VCR 6.2.0 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/rename_file_with_location_success.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/rename_file_with_location_success.yml new file mode 100644 index 000000000000..66af7a90012e --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/rename_file_with_location_success.yml @@ -0,0 +1,243 @@ +--- +http_interactions: +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/167 + body: + encoding: US-ASCII + string: '' + headers: + Ocs-Apirequest: + - 'true' + Accept: + - application/json + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.2.6 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 24 Jun 2024 12:47:44 GMT + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Pragma: + - no-cache + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.59 (Debian) + Set-Cookie: + - oc07ul6b4oaw=abde6d1d4a1b1c4c840e8d23a169e2ab; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=iMCcKhvw%2FOWQYaplVk42UJ1rXRafmGWYkl51rG%2FdbufkEkmafMHmURfSJsL6fGTLh4mBKKPQ0KSjmRhMJCfL2yWy5I36yHMsmXc4PckhStdLSrBQWGdZlXLeYUz0Neip; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=abde6d1d4a1b1c4c840e8d23a169e2ab; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=abde6d1d4a1b1c4c840e8d23a169e2ab; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=abde6d1d4a1b1c4c840e8d23a169e2ab; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=abde6d1d4a1b1c4c840e8d23a169e2ab; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=abde6d1d4a1b1c4c840e8d23a169e2ab; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=abde6d1d4a1b1c4c840e8d23a169e2ab; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=abde6d1d4a1b1c4c840e8d23a169e2ab; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=abde6d1d4a1b1c4c840e8d23a169e2ab; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.18 + X-Request-Id: + - 0g2PLntU1vmpu9XWTV2C + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '262' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":167,"name":"request_001.md","mtime":1701268524,"ctime":0,"mimetype":"text\/markdown","size":48,"owner_name":"admin","owner_id":"admin","modifier_name":null,"modifier_id":null,"dav_permissions":"RGDNVW","path":"files\/Folder + with spaces\/New Requests\/request_001.md"}}}' + recorded_at: Mon, 24 Jun 2024 12:47:44 GMT +- request: + method: move + uri: https://nextcloud.local/remote.php/dav/files/admin/Folder%20with%20spaces/New%20Requests/request_001.md + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + Destination: + - https://nextcloud.local/remote.php/dav/files/admin/Folder%20with%20spaces/New%20Requests/I%E2%9D%A4%EF%B8%8Fyou%20death%20star.md + User-Agent: + - httpx.rb/1.2.6 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Cache-Control: + - no-store, no-cache, must-revalidate + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Mon, 24 Jun 2024 12:47:44 GMT + Etag: + - '"ef9970d784b65aac545b0e0cc5060536"' + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Oc-Etag: + - '"ef9970d784b65aac545b0e0cc5060536"' + Oc-Fileid: + - 00000167oc07ul6b4oaw + Pragma: + - no-cache + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.59 (Debian) + Set-Cookie: + - oc07ul6b4oaw=67763dcd669717ca18ad461ae3af289d; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=hBRu%2FMnoJSkD%2FQZ94jyQkPXBKsVUeP2cuZXIbchQyd%2Bqn%2Fm4nWkdKSvF9sWkKiW3zc1KJ4T7x882HabOlQHvLCXqfthg9E1iBa8MptrT46S46AZUQPJaL8VR8UjwF%2FO3; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=67763dcd669717ca18ad461ae3af289d; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=67763dcd669717ca18ad461ae3af289d; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=67763dcd669717ca18ad461ae3af289d; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=67763dcd669717ca18ad461ae3af289d; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=67763dcd669717ca18ad461ae3af289d; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=67763dcd669717ca18ad461ae3af289d; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=67763dcd669717ca18ad461ae3af289d; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=67763dcd669717ca18ad461ae3af289d; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 3yszavGzGnTgraffEGd2 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.18 + X-Request-Id: + - 3yszavGzGnTgraffEGd2 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Mon, 24 Jun 2024 12:47:44 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/167 + body: + encoding: US-ASCII + string: '' + headers: + Ocs-Apirequest: + - 'true' + Accept: + - application/json + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.2.6 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 24 Jun 2024 12:47:44 GMT + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Pragma: + - no-cache + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.59 (Debian) + Set-Cookie: + - oc07ul6b4oaw=f1f030cd937c8ceddde96d1b57cec45d; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=49eHjlMqrmvl2%2FTwHT9UzCdNHA8gJxMl4%2B8RlWVinAmbTLYxpcWnnJ0%2Fc34LMsiWAet3YP5Yx3MWoww%2FUejUGNPaYTyhJC5Pm8KPRiTA35sPW3hMV5UZ6GD%2FJJadis%2BZ; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=f1f030cd937c8ceddde96d1b57cec45d; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=f1f030cd937c8ceddde96d1b57cec45d; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=f1f030cd937c8ceddde96d1b57cec45d; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=f1f030cd937c8ceddde96d1b57cec45d; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=f1f030cd937c8ceddde96d1b57cec45d; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=f1f030cd937c8ceddde96d1b57cec45d; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=f1f030cd937c8ceddde96d1b57cec45d; + path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=f1f030cd937c8ceddde96d1b57cec45d; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.18 + X-Request-Id: + - iBuJ005jfGLn9Pj3qRff + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '277' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":167,"name":"I\u2764\ufe0fyou + death star.md","mtime":1701268524,"ctime":0,"mimetype":"text\/markdown","size":48,"owner_name":"admin","owner_id":"admin","modifier_name":null,"modifier_id":null,"dav_permissions":"RGDNVW","path":"files\/Folder + with spaces\/New Requests\/I\u2764\ufe0fyou death star.md"}}}' + recorded_at: Mon, 24 Jun 2024 12:47:44 GMT +recorded_with: VCR 6.2.0 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/rename_file_not_found.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/rename_file_not_found.yml new file mode 100644 index 000000000000..3360e69fcbcd --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/rename_file_not_found.yml @@ -0,0 +1,106 @@ +--- +http_interactions: +- request: + method: post + uri: https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token + body: + encoding: ASCII-8BIT + string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=4262df2b-77bb-49c2-a5df-28355da676d2&client_secret=Vwk8Q%7EJTuPh.pAjvPiWBQBdTFMDK%7EAIwxbj9_axB + headers: + User-Agent: + - httpx.rb/1.2.6 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '201' + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Expires: + - "-1" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + P3p: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + X-Ms-Request-Id: + - 8deae3a0-c4d2-41ea-8803-af5debd34e00 + X-Ms-Ests-Server: + - 2.1.18298.5 - WEULR1 ProdSlices + X-Ms-Srs: + - 1.P + X-Xss-Protection: + - '0' + Set-Cookie: + - fpc=AmVwpcESbh5GrfFjS0tnz3ekbDoXAQAAAPBUC94OAAAA; expires=Wed, 24-Jul-2024 + 11:52:17 GMT; path=/; secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; + path=/; secure; samesite=none; httponly, stsservicecookie=estsfd; path=/; + secure; samesite=none; httponly + Date: + - Mon, 24 Jun 2024 11:52:17 GMT + Content-Length: + - '1759' + body: + encoding: UTF-8 + string: '{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":""}' + recorded_at: Mon, 24 Jun 2024 11:52:17 GMT +- request: + method: patch + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/items/sith_have_yellow_light_sabers + body: + encoding: UTF-8 + string: '{"name":"this_will_not_happen.png"}' + headers: + Content-Type: + - application/json + User-Agent: + - httpx.rb/1.2.6 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Length: + - '35' + Authorization: + - Bearer + response: + status: + code: 404 + message: Not Found + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json + Content-Encoding: + - gzip + Vary: + - Accept-Encoding + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - e24dfe38-bac8-4f32-a8fc-6a79c0b20e92 + Client-Request-Id: + - e24dfe38-bac8-4f32-a8fc-6a79c0b20e92 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"004","RoleInstance":"FR2PEPF0000056F"}}' + Date: + - Mon, 24 Jun 2024 11:52:17 GMT + body: + encoding: UTF-8 + string: '{"error":{"code":"itemNotFound","message":"Item not found","innerError":{"date":"2024-06-24T11:52:18","request-id":"e24dfe38-bac8-4f32-a8fc-6a79c0b20e92","client-request-id":"e24dfe38-bac8-4f32-a8fc-6a79c0b20e92"}}}' + recorded_at: Mon, 24 Jun 2024 11:52:18 GMT +recorded_with: VCR 6.2.0 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/rename_file_success.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/rename_file_success.yml new file mode 100644 index 000000000000..2378f2a4df89 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/rename_file_success.yml @@ -0,0 +1,110 @@ +--- +http_interactions: +- request: + method: post + uri: https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token + body: + encoding: ASCII-8BIT + string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=4262df2b-77bb-49c2-a5df-28355da676d2&client_secret=Vwk8Q%7EJTuPh.pAjvPiWBQBdTFMDK%7EAIwxbj9_axB + headers: + User-Agent: + - httpx.rb/1.2.6 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '201' + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Expires: + - "-1" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + P3p: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + X-Ms-Request-Id: + - 82fd081e-b88e-41ce-bf74-b01ddaee3c00 + X-Ms-Ests-Server: + - 2.1.18298.5 - NEULR1 ProdSlices + X-Ms-Srs: + - 1.P + X-Xss-Protection: + - '0' + Set-Cookie: + - fpc=Am--ZGqggClMv2Xq9vAANLCkbDoXAQAAAJFTC94OAAAA; expires=Wed, 24-Jul-2024 + 11:46:25 GMT; path=/; secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; + path=/; secure; samesite=none; httponly, stsservicecookie=estsfd; path=/; + secure; samesite=none; httponly + Date: + - Mon, 24 Jun 2024 11:46:25 GMT + Content-Length: + - '1760' + body: + encoding: UTF-8 + string: '{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":""}' + recorded_at: Mon, 24 Jun 2024 11:46:25 GMT +- request: + method: patch + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/items/01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR + body: + encoding: UTF-8 + string: '{"name":"I am the senat"}' + headers: + Content-Type: + - application/json + User-Agent: + - httpx.rb/1.2.6 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Length: + - '25' + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 + Content-Encoding: + - gzip + Vary: + - Accept-Encoding + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - fa6635a9-b6db-4f9b-af49-e12958e536b0 + Client-Request-Id: + - fa6635a9-b6db-4f9b-af49-e12958e536b0 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"002","RoleInstance":"FR3PEPF0000055B"}}' + Odata-Version: + - '4.0' + Date: + - Mon, 24 Jun 2024 11:46:26 GMT + body: + encoding: UTF-8 + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#drives(''b%21dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs'')/items/$entity","createdDateTime":"2023-09-26T14:38:50Z","eTag":"\"{6087B980-4C01-4020-BBF2-1E349BD0C831},2\"","id":"01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR","lastModifiedDateTime":"2024-06-24T11:46:27Z","name":"I + am the senat","webUrl":"https://finn.sharepoint.com/sites/openprojectfilestoragetests/VCR/I%20am%20the%20senat","cTag":"\"c:{6087B980-4C01-4020-BBF2-1E349BD0C831},0\"","size":263282,"createdBy":{"user":{"email":"eschubert.op@outlook.com","id":"0a0d38a9-a59b-4245-93fa-0d2cf727f17a","displayName":"Eric + Schubert"}},"lastModifiedBy":{"user":{"displayName":"SharePoint App"}},"parentReference":{"driveType":"documentLibrary","driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","id":"01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ","name":"VCR","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","siteId":"1b4b6576-906d-4d94-8f49-6d00a9507b50"},"fileSystemInfo":{"createdDateTime":"2023-09-26T14:38:50Z","lastModifiedDateTime":"2024-06-24T11:46:27Z"},"folder":{"childCount":5},"shared":{"scope":"users"}}' + recorded_at: Mon, 24 Jun 2024 11:46:26 GMT +recorded_with: VCR 6.2.0 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/rename_file_with_location_success.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/rename_file_with_location_success.yml new file mode 100644 index 000000000000..295a2bf65f8a --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/rename_file_with_location_success.yml @@ -0,0 +1,112 @@ +--- +http_interactions: +- request: + method: post + uri: https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token + body: + encoding: ASCII-8BIT + string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=4262df2b-77bb-49c2-a5df-28355da676d2&client_secret=Vwk8Q%7EJTuPh.pAjvPiWBQBdTFMDK%7EAIwxbj9_axB + headers: + User-Agent: + - httpx.rb/1.2.6 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '201' + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Expires: + - "-1" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + P3p: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + X-Ms-Request-Id: + - ffa8f490-b965-4f8e-bc76-cadd36557600 + X-Ms-Ests-Server: + - 2.1.18298.5 - FRC ProdSlices + X-Ms-Srs: + - 1.P + X-Xss-Protection: + - '0' + Set-Cookie: + - fpc=ArnOt0kiO4BGuweLgTpTFIykbDoXAQAAAGdUC94OAAAA; expires=Wed, 24-Jul-2024 + 11:49:59 GMT; path=/; secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; + path=/; secure; samesite=none; httponly, stsservicecookie=estsfd; path=/; + secure; samesite=none; httponly + Date: + - Mon, 24 Jun 2024 11:49:58 GMT + Content-Length: + - '1760' + body: + encoding: UTF-8 + string: '{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":""}' + recorded_at: Mon, 24 Jun 2024 11:49:59 GMT +- request: + method: patch + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/items/01AZJL5PPMSBBO3R2BIZHJFCELSW3RP7GN + body: + encoding: UTF-8 + string: '{"name":"I❤️you death star.png"}' + headers: + Content-Type: + - application/json + User-Agent: + - httpx.rb/1.2.6 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Length: + - '37' + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 + Content-Encoding: + - gzip + Vary: + - Accept-Encoding + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - e3c6e74c-eccc-46f3-9a5a-95710b29e084 + Client-Request-Id: + - e3c6e74c-eccc-46f3-9a5a-95710b29e084 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"005","RoleInstance":"FR3PEPF000005F4"}}' + Odata-Version: + - '4.0' + Date: + - Mon, 24 Jun 2024 11:50:00 GMT + body: + encoding: UTF-8 + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#drives(''b%21dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs'')/items/$entity","@microsoft.graph.downloadUrl":"https://finn.sharepoint.com/sites/openprojectfilestoragetests/_layouts/15/download.aspx?UniqueId=ed4290ec-41c7-4e46-9288-8b95b717fccd&Translate=false&tempauth=v1.eyJzaXRlaWQiOiIxYjRiNjU3Ni05MDZkLTRkOTQtOGY0OS02ZDAwYTk1MDdiNTAiLCJhcHBfZGlzcGxheW5hbWUiOiJPcGVuUHJvamVjdCBEZXYgQXBwIiwiYXVkIjoiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwL2Zpbm4uc2hhcmVwb2ludC5jb21ANGQ0NGJmMzYtOWI1Ni00NWMwLTg4MDctYmJmMzg2ZGQwNDdmIiwiZXhwIjoiMTcxOTIzMzQwMCJ9.EgsI4u6x9rmTij0QBRoNMjAuMTkwLjE5MC45OSosZ2VLeEg1cUh3Qi81RVJudlp1UC9xOUJYUlBoUkRoT0ZMWjByUXppS2FtTT0wlQE4AUIQoTXEG28gAJAeSuUTap6OE0oQaGFzaGVkcHJvb2Z0b2tlbnoBMboBK2FsbHNpdGVzLnJlYWQgYWxsc2l0ZXMud3JpdGUgYWxsZmlsZXMud3JpdGXCAUk0MjYyZGYyYi03N2JiLTQ5YzItYTVkZi0yODM1NWRhNjc2ZDJANGQ0NGJmMzYtOWI1Ni00NWMwLTg4MDctYmJmMzg2ZGQwNDdmyAEB.m_odT3aPits40RUzjqhPjF9Y-MvkduKxqPj5ihmoHDU&ApiVersion=2.0","createdDateTime":"2023-09-26T14:40:27Z","eTag":"\"{ED4290EC-41C7-4E46-9288-8B95B717FCCD},4\"","id":"01AZJL5PPMSBBO3R2BIZHJFCELSW3RP7GN","lastModifiedDateTime":"2024-06-24T11:50:00Z","name":"I\u2764\ufe0fyou + death star.png","webUrl":"https://finn.sharepoint.com/sites/openprojectfilestoragetests/VCR/Folder%20with%20spaces/I%E2%9D%A4%EF%B8%8Fyou%20death%20start.png","cTag":"\"c:{ED4290EC-41C7-4E46-9288-8B95B717FCCD},3\"","size":9482,"createdBy":{"user":{"email":"eschubert.op@outlook.com","id":"0a0d38a9-a59b-4245-93fa-0d2cf727f17a","displayName":"Eric + Schubert"}},"lastModifiedBy":{"user":{"displayName":"SharePoint App"}},"parentReference":{"driveType":"documentLibrary","driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","id":"01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU","name":"Folder + with spaces","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:/Folder + with spaces","siteId":"1b4b6576-906d-4d94-8f49-6d00a9507b50"},"file":{"mimeType":"image/png","hashes":{"quickXorHash":"58goJ0v735ljlyoxrwUj5+78tHc="}},"fileSystemInfo":{"createdDateTime":"2023-09-26T14:40:27Z","lastModifiedDateTime":"2024-06-24T11:50:00Z"},"image":{"height":419,"width":347},"photo":{},"shared":{"scope":"users"}}' + recorded_at: Mon, 24 Jun 2024 11:50:00 GMT +recorded_with: VCR 6.2.0 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/rename_folder_success.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/rename_folder_success.yml deleted file mode 100644 index 334975dfeab1..000000000000 --- a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/rename_folder_success.yml +++ /dev/null @@ -1,260 +0,0 @@ ---- -http_interactions: -- request: - method: post - uri: https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token - body: - encoding: ASCII-8BIT - string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=4262df2b-77bb-49c2-a5df-28355da676d2&client_secret=Vwk8Q%7EJTuPh.pAjvPiWBQBdTFMDK%7EAIwxbj9_axB - headers: - User-Agent: - - httpx.rb/1.2.5 - Accept: - - "*/*" - Accept-Encoding: - - gzip, deflate - Content-Type: - - application/x-www-form-urlencoded - Content-Length: - - '201' - response: - status: - code: 200 - message: OK - headers: - Cache-Control: - - no-store, no-cache - Pragma: - - no-cache - Content-Type: - - application/json; charset=utf-8 - Expires: - - "-1" - Strict-Transport-Security: - - max-age=31536000; includeSubDomains - X-Content-Type-Options: - - nosniff - P3p: - - CP="DSP CUR OTPi IND OTRi ONL FIN" - X-Ms-Request-Id: - - 5d94463c-89b2-4148-8afa-47d6d7686900 - X-Ms-Ests-Server: - - 2.1.18216.5 - SEC ProdSlices - X-Ms-Srs: - - 1.P - X-Xss-Protection: - - '0' - Set-Cookie: - - fpc=ApsGOKKY9c5MtcLEnQSVmSqkbDoXAQAAAJkC-d0OAAAA; expires=Wed, 10-Jul-2024 - 14:20:10 GMT; path=/; secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; - path=/; secure; samesite=none; httponly, stsservicecookie=estsfd; path=/; - secure; samesite=none; httponly - Date: - - Mon, 10 Jun 2024 14:20:09 GMT - Content-Length: - - '1735' - body: - encoding: UTF-8 - string: '{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":""}' - recorded_at: Mon, 10 Jun 2024 14:20:10 GMT -- request: - method: post - uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root/children - body: - encoding: UTF-8 - string: '{"name":"Wrong Name","folder":{},"@microsoft.graph.conflictBehavior":"fail"}' - headers: - Content-Type: - - application/json - User-Agent: - - httpx.rb/1.2.5 - Accept: - - "*/*" - Accept-Encoding: - - gzip, deflate - Content-Length: - - '76' - Authorization: - - Bearer - response: - status: - code: 201 - message: Created - headers: - Cache-Control: - - no-store, no-cache - Content-Type: - - application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 - Content-Encoding: - - gzip - Etag: - - '"{B880A739-43EF-4363-9134-BD49CFE7C910},1"' - Location: - - https://finn.sharepoint.com/_api/v2.0/drives('b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs')/items('root')/children('01AZJL5PJZU6ALR32DMNBZCNF5JHH6PSIQ') - Vary: - - Accept-Encoding - Strict-Transport-Security: - - max-age=31536000 - Request-Id: - - cd98c895-8ec4-45b7-8f07-eca6799a38c2 - Client-Request-Id: - - cd98c895-8ec4-45b7-8f07-eca6799a38c2 - X-Ms-Ags-Diagnostic: - - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"005","RoleInstance":"FR3PEPF000005B8"}}' - Odata-Version: - - '4.0' - Date: - - Mon, 10 Jun 2024 14:20:10 GMT - body: - encoding: UTF-8 - string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#drives(''b%21dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs'')/root/children/$entity","@odata.etag":"\"{B880A739-43EF-4363-9134-BD49CFE7C910},1\"","createdDateTime":"2024-06-10T14:20:11Z","eTag":"\"{B880A739-43EF-4363-9134-BD49CFE7C910},1\"","id":"01AZJL5PJZU6ALR32DMNBZCNF5JHH6PSIQ","lastModifiedDateTime":"2024-06-10T14:20:11Z","name":"Wrong - Name","size":0,"webUrl":"https://finn.sharepoint.com/sites/openprojectfilestoragetests/VCR/Wrong%20Name","cTag":"\"c:{B880A739-43EF-4363-9134-BD49CFE7C910},0\"","commentSettings":{"commentingDisabled":{"isDisabled":false}},"createdBy":{"application":{"displayName":"OpenProject - Dev App","id":"4262df2b-77bb-49c2-a5df-28355da676d2"},"user":{"displayName":"SharePoint - App"}},"lastModifiedBy":{"application":{"displayName":"OpenProject Dev App","id":"4262df2b-77bb-49c2-a5df-28355da676d2"},"user":{"displayName":"SharePoint - App"}},"parentReference":{"driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","driveType":"documentLibrary","id":"01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","sharepointIds":{"listId":"f3baf95b-362b-4740-80d8-4f593d28f5ec","listItemUniqueId":"049e81d0-52fb-4624-af6d-96611c29a9cc","siteId":"1b4b6576-906d-4d94-8f49-6d00a9507b50","siteUrl":"https://finn.sharepoint.com/sites/openprojectfilestoragetests","tenantId":"4d44bf36-9b56-45c0-8807-bbf386dd047f","webId":"7ef259e8-8eed-4645-920a-8b367bb0d8e0"}},"fileSystemInfo":{"createdDateTime":"2024-06-10T14:20:11Z","lastModifiedDateTime":"2024-06-10T14:20:11Z"},"folder":{"childCount":0},"shared":{"scope":"unknown"}}' - recorded_at: Mon, 10 Jun 2024 14:20:11 GMT -- request: - method: post - uri: https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token - body: - encoding: UTF-8 - string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default - headers: - User-Agent: - - Rack::OAuth2 (2.2.1) - Authorization: - - Basic - Content-Type: - - application/x-www-form-urlencoded - Accept-Encoding: - - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - Accept: - - "*/*" - response: - status: - code: 200 - message: OK - headers: - Cache-Control: - - no-store, no-cache - Pragma: - - no-cache - Content-Type: - - application/json; charset=utf-8 - Expires: - - "-1" - Strict-Transport-Security: - - max-age=31536000; includeSubDomains - X-Content-Type-Options: - - nosniff - P3p: - - CP="DSP CUR OTPi IND OTRi ONL FIN" - X-Ms-Request-Id: - - bb546166-785b-453c-81bc-cb8c93af6e00 - X-Ms-Ests-Server: - - 2.1.18216.5 - SEC ProdSlices - X-Ms-Srs: - - 1.P - X-Xss-Protection: - - '0' - Set-Cookie: - - fpc=AhKGA6Jv29JLr32JHmFcFrukbDoXAQAAAJoC-d0OAAAA; expires=Wed, 10-Jul-2024 - 14:20:11 GMT; path=/; secure; HttpOnly; SameSite=None - - stsservicecookie=estsfd; path=/; secure; samesite=none; httponly - - x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly - Date: - - Mon, 10 Jun 2024 14:20:11 GMT - Content-Length: - - '1708' - body: - encoding: UTF-8 - string: '{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":""}' - recorded_at: Mon, 10 Jun 2024 14:20:11 GMT -- request: - method: patch - uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/items/01AZJL5PJZU6ALR32DMNBZCNF5JHH6PSIQ - body: - encoding: UTF-8 - string: '{"name":"My Project No. 1 (19)"}' - headers: - Authorization: - - Bearer - Accept: - - application/json - Content-Type: - - application/json - User-Agent: - - httpx.rb/1.2.5 - Accept-Encoding: - - gzip, deflate - Content-Length: - - '32' - response: - status: - code: 200 - message: OK - headers: - Cache-Control: - - no-store, no-cache - Content-Type: - - application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 - Content-Encoding: - - gzip - Vary: - - Accept-Encoding - Strict-Transport-Security: - - max-age=31536000 - Request-Id: - - 6ec63b53-7694-4127-91f2-6c9cc9741bd5 - Client-Request-Id: - - 6ec63b53-7694-4127-91f2-6c9cc9741bd5 - X-Ms-Ags-Diagnostic: - - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"005","RoleInstance":"FR3PEPF000002E4"}}' - Odata-Version: - - '4.0' - Date: - - Mon, 10 Jun 2024 14:20:11 GMT - body: - encoding: UTF-8 - string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#drives(''b%21dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs'')/items/$entity","createdDateTime":"2024-06-10T14:20:11Z","eTag":"\"{B880A739-43EF-4363-9134-BD49CFE7C910},3\"","id":"01AZJL5PJZU6ALR32DMNBZCNF5JHH6PSIQ","lastModifiedDateTime":"2024-06-10T14:20:12Z","name":"My - Project No. 1 (19)","webUrl":"https://finn.sharepoint.com/sites/openprojectfilestoragetests/VCR/My%20Project%20No.%201%20(19)","cTag":"\"c:{B880A739-43EF-4363-9134-BD49CFE7C910},0\"","size":0,"createdBy":{"application":{"id":"4262df2b-77bb-49c2-a5df-28355da676d2","displayName":"OpenProject - Dev App"},"user":{"displayName":"SharePoint App"}},"lastModifiedBy":{"application":{"id":"4262df2b-77bb-49c2-a5df-28355da676d2","displayName":"OpenProject - Dev App"},"user":{"displayName":"SharePoint App"}},"parentReference":{"driveType":"documentLibrary","driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","id":"01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ","name":"VCR","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","siteId":"1b4b6576-906d-4d94-8f49-6d00a9507b50"},"fileSystemInfo":{"createdDateTime":"2024-06-10T14:20:11Z","lastModifiedDateTime":"2024-06-10T14:20:12Z"},"folder":{"childCount":0},"shared":{"scope":"users"}}' - recorded_at: Mon, 10 Jun 2024 14:20:12 GMT -- request: - method: delete - uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/items/01AZJL5PJZU6ALR32DMNBZCNF5JHH6PSIQ - body: - encoding: US-ASCII - string: '' - headers: - Authorization: - - Bearer - User-Agent: - - httpx.rb/1.2.5 - Accept: - - "*/*" - Accept-Encoding: - - gzip, deflate - response: - status: - code: 204 - message: No Content - headers: - Cache-Control: - - no-store, no-cache - Strict-Transport-Security: - - max-age=31536000 - Request-Id: - - b6c9de3e-fb1a-428a-bac3-158a753dae5d - Client-Request-Id: - - b6c9de3e-fb1a-428a-bac3-158a753dae5d - X-Ms-Ags-Diagnostic: - - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"005","RoleInstance":"FR3PEPF00000553"}}' - Date: - - Mon, 10 Jun 2024 14:20:11 GMT - body: - encoding: UTF-8 - string: '' - recorded_at: Mon, 10 Jun 2024 14:20:12 GMT -recorded_with: VCR 6.2.0 diff --git a/modules/storages/spec/support/shared_examples_for_adapters/rename_file_command_examples.rb b/modules/storages/spec/support/shared_examples_for_adapters/rename_file_command_examples.rb new file mode 100644 index 000000000000..e22ea38bc775 --- /dev/null +++ b/modules/storages/spec/support/shared_examples_for_adapters/rename_file_command_examples.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +RSpec.shared_examples_for "rename_file_command: basic command setup" do + it "is registered as commands.rename_file" do + expect(Storages::Peripherals::Registry + .resolve("#{storage.short_provider_type}.commands.rename_file")).to eq(described_class) + end + + it "responds to #call with correct parameters" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], + %i[keyreq auth_strategy], + %i[keyreq file_id], + %i[keyreq name]) + end +end + +RSpec.shared_examples_for "rename_file_command: successful file renaming" do + it "returns success and the renamed file" do + result = described_class.call(storage:, auth_strategy:, file_id:, name:) + + expect(result).to be_success + + response = result.result + expect(response).to be_a(Storages::StorageFile) + expect(response.id).to eq(file_id) + expect(response.name).to eq(name) + end +end + +RSpec.shared_examples_for "rename_file_command: not found" do + it "returns a failure" do + result = described_class.call(storage:, auth_strategy:, file_id:, name:) + + expect(result).to be_failure + + error = result.errors + expect(error.code).to eq(:not_found) + expect(error.data.source).to eq(error_source) + end +end + +RSpec.shared_examples_for "rename_file_command: error" do + it "returns a failure" do + result = described_class.call(storage:, auth_strategy:, file_id:, name:) + + expect(result).to be_failure + + error = result.errors + expect(error.code).to eq(:error) + expect(error.data.source).to eq(error_source) + end +end + +RSpec.shared_examples_for "rename_file_command: validating input data" do + let(:error_source) { described_class } + let(:file_id) { "my_file" } + let(:name) { "new_name" } + + context "if file_id is empty" do + let(:file_id) { "" } + + it_behaves_like "rename_file_command: error" + end + + context "if name is empty" do + let(:name) { "" } + + it_behaves_like "rename_file_command: error" + end +end diff --git a/modules/storages/spec/workers/storages/automatically_managed_storage_sync_job_spec.rb b/modules/storages/spec/workers/storages/automatically_managed_storage_sync_job_spec.rb new file mode 100644 index 000000000000..81a961f2b4cc --- /dev/null +++ b/modules/storages/spec/workers/storages/automatically_managed_storage_sync_job_spec.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require_module_spec_helper + +RSpec.describe Storages::AutomaticallyManagedStorageSyncJob, type: :job do + let(:managed_nextcloud) { create(:nextcloud_storage_configured, :as_automatically_managed) } + + describe ".debounce" do + context "when has been debounced by other thread" do + it "does not change the number of enqueued jobs" do + expect(performed_jobs.count).to eq(0) + expect(described_class.debounce(managed_nextcloud).successfully_enqueued?).to be(true) + expect(described_class.debounce(managed_nextcloud)).to be(false) + expect(enqueued_jobs.count).to eq(1) + + expect { described_class.debounce(managed_nextcloud) }.not_to change(enqueued_jobs, :count) + end + end + + context "when has not been debounced by other thread" do + before { RequestStore.delete("sync-nextcloud-#{managed_nextcloud.id}") } + + it "schedules a job" do + expect { described_class.debounce(managed_nextcloud) }.to change(enqueued_jobs, :count).from(0).to(1) + end + end + end + + describe ".perform" do + subject(:job_instance) { described_class.new } + + it "only runs for automatically managed storages" do + unmanaged_nextcloud = create(:nextcloud_storage_configured, :as_not_automatically_managed) + + allow(Storages::NextcloudGroupFolderPropertiesSyncService) + .to receive(:call).with(managed_nextcloud).and_return(ServiceResult.success) + + job_instance.perform(managed_nextcloud) + job_instance.perform(unmanaged_nextcloud) + + expect(Storages::NextcloudGroupFolderPropertiesSyncService).to have_received(:call).with(managed_nextcloud) + expect(Storages::NextcloudGroupFolderPropertiesSyncService).not_to have_received(:call).with(unmanaged_nextcloud) + end + + it "marks storage as healthy if sync was successful" do + allow(Storages::NextcloudGroupFolderPropertiesSyncService) + .to receive(:call).with(managed_nextcloud).and_return(ServiceResult.success) + + Timecop.freeze("2023-03-14T15:17:00Z") do + expect do + job_instance.perform(managed_nextcloud) + managed_nextcloud.reload + end.to( + change(managed_nextcloud, :health_changed_at).to(Time.now.utc) + .and(change(managed_nextcloud, :health_status).from("pending").to("healthy")) + ) + end + end + + it "marks storage as unhealthy if sync was unsuccessful" do + job = class_double(Storages::HealthStatusMailerJob) + allow(Storages::HealthStatusMailerJob).to receive(:set).and_return(job) + allow(job).to receive(:perform_later) + + allow(Storages::NextcloudGroupFolderPropertiesSyncService) + .to receive(:call) + .with(managed_nextcloud) + .and_return(ServiceResult.failure(errors: Storages::StorageError.new(code: :not_found))) + + Timecop.freeze("2023-03-14T15:17:00Z") do + expect do + perform_enqueued_jobs { described_class.perform_later(managed_nextcloud) } + managed_nextcloud.reload + end.to( + change(managed_nextcloud, :health_changed_at).to(Time.now.utc) + .and(change(managed_nextcloud, :health_status).from("pending").to("unhealthy")) + .and(change(managed_nextcloud, :health_reason).from(nil).to("not_found")) + ) + end + end + + context "when Storages::Errors::IntegrationJobError is raised" do + before do + allow(Storages::NextcloudGroupFolderPropertiesSyncService) + .to receive(:call).with(managed_nextcloud) + .and_return(ServiceResult.failure(errors: Storages::StorageError.new(code: :custom_error))) + + allow(OpenProject::Notifications).to receive(:send) + end + + it "retries the job" do + perform_enqueued_jobs { described_class.perform_later(managed_nextcloud) } + performed_jobs = described_class.queue_adapter.performed_jobs + + expect(performed_jobs.last.dig("exception_executions", "[Storages::Errors::IntegrationJobError]")).to eq(5) + end + + it "sends a notification after the maximum number of attempts" do + perform_enqueued_jobs { described_class.perform_later(managed_nextcloud) } + + expect(OpenProject::Notifications).to have_received(:send).with( + OpenProject::Events::STORAGE_TURNED_UNHEALTHY, + storage: managed_nextcloud, + reason: "custom_error" + ) + end + end + end +end diff --git a/modules/storages/spec/workers/storages/manage_storage_integrations_job_spec.rb b/modules/storages/spec/workers/storages/manage_storage_integrations_job_spec.rb index cb46183921d7..5ae11b91dd1b 100644 --- a/modules/storages/spec/workers/storages/manage_storage_integrations_job_spec.rb +++ b/modules/storages/spec/workers/storages/manage_storage_integrations_job_spec.rb @@ -31,7 +31,7 @@ require "spec_helper" require_module_spec_helper -RSpec.describe Storages::ManageStorageIntegrationsJob, :webmock, type: :job do +RSpec.describe Storages::ManageStorageIntegrationsJob, type: :job do describe ".debounce" do context "when has been debounced by other thread" do before { ActiveJob::Base.disable_test_adapter } @@ -65,8 +65,6 @@ describe ".disable_cron_job_if_needed" do before { ActiveJob::Base.disable_test_adapter } - subject { described_class.disable_cron_job_if_needed } - context "when there is an active nextcloud project storage" do shared_let(:storage1) { create(:nextcloud_storage, :as_automatically_managed) } shared_let(:project_storage) { create(:project_storage, :as_automatically_managed, storage: storage1) } @@ -78,7 +76,7 @@ expect(good_job_setting.key).to eq("cron_keys_disabled") expect(good_job_setting.value).to eq(["Storages::ManageStorageIntegrationsJob"]) - expect { subject }.not_to change(GoodJob::Setting, :count).from(1) + expect { described_class.disable_cron_job_if_needed }.not_to change(GoodJob::Setting, :count).from(1) good_job_setting.reload expect(good_job_setting.key).to eq("cron_keys_disabled") @@ -88,7 +86,7 @@ it "does nothing if the cron_job is not disabled" do expect(GoodJob::Setting.cron_key_enabled?(described_class::CRON_JOB_KEY)).to be(true) - expect { subject }.not_to change(GoodJob::Setting, :count).from(0) + expect { described_class.disable_cron_job_if_needed }.not_to change(GoodJob::Setting, :count).from(0) expect(GoodJob::Setting.cron_key_enabled?(described_class::CRON_JOB_KEY)).to be(true) end @@ -96,7 +94,7 @@ context "when there is no active nextcloud project storage" do it "disables the cron job" do - expect { subject }.to change(GoodJob::Setting, :count).from(0).to(1) + expect { described_class.disable_cron_job_if_needed }.to change(GoodJob::Setting, :count).from(0).to(1) good_job_setting = GoodJob::Setting.first expect(good_job_setting.key).to eq("cron_keys_disabled") @@ -106,89 +104,14 @@ end describe ".perform" do - let(:storage1) { create(:nextcloud_storage_configured, :as_automatically_managed) } - - subject { described_class.new.perform } - - it "calls NextcloudGroupFolderPropertiesSyncService for each automatically managed storage" do - storage2 = create(:nextcloud_storage, :as_not_automatically_managed) - storage3 = create(:nextcloud_storage, :as_automatically_managed) - - allow(Storages::NextcloudGroupFolderPropertiesSyncService) - .to receive(:call).with(storage1).and_return(ServiceResult.success) - - subject - - expect(Storages::NextcloudGroupFolderPropertiesSyncService).to have_received(:call).with(storage1).once - expect(Storages::NextcloudGroupFolderPropertiesSyncService).not_to have_received(:call).with(storage2) - expect(Storages::NextcloudGroupFolderPropertiesSyncService).not_to have_received(:call).with(storage3) - end - - it "marks storage as healthy if sync was successful" do - allow(Storages::NextcloudGroupFolderPropertiesSyncService) - .to receive(:call).with(storage1).and_return(ServiceResult.success) - - Timecop.freeze("2023-03-14T15:17:00Z") do - expect do - subject - storage1.reload - end.to( - change(storage1, :health_changed_at).to(Time.now.utc) - .and(change(storage1, :health_status).from("pending").to("healthy")) - ) - end - end - - it "marks storage as unhealthy if sync was unsuccessful" do - job = class_double(Storages::HealthStatusMailerJob) - allow(Storages::HealthStatusMailerJob).to receive(:set).and_return(job) - allow(job).to receive(:perform_later) - - allow(Storages::NextcloudGroupFolderPropertiesSyncService) - .to receive(:call) - .with(storage1) - .and_return(ServiceResult.failure(errors: Storages::StorageError.new(code: :not_found))) - - Timecop.freeze("2023-03-14T15:17:00Z") do - expect do - perform_enqueued_jobs { described_class.perform_later } - storage1.reload - end.to( - change(storage1, :health_changed_at).to(Time.now.utc) - .and(change(storage1, :health_status).from("pending").to("unhealthy")) - .and(change(storage1, :health_reason).from(nil).to("not_found")) - ) - end + before do + create(:nextcloud_storage_configured, :as_automatically_managed) + create(:nextcloud_storage, :as_not_automatically_managed) + create(:sharepoint_dev_drive_storage, :as_automatically_managed) end - context "when Storages::Errors::IntegrationJobError is raised" do - before do - allow(Storages::NextcloudGroupFolderPropertiesSyncService) - .to receive(:call).with(storage1) - .and_return(ServiceResult.failure(errors: Storages::StorageError.new(code: :custom_error))) - end - - it "retries the job" do - allow(OpenProject::Notifications).to receive(:send) - - perform_enqueued_jobs { described_class.perform_later } - - expect(described_class - .queue_adapter.performed_jobs - .last.dig("exception_executions", "[Storages::Errors::IntegrationJobError]")).to eq(5) - end - - it "sends a notification after the maximum number of attempts" do - allow(OpenProject::Notifications).to receive(:send) - - perform_enqueued_jobs { described_class.perform_later } - - expect(OpenProject::Notifications).to have_received(:send).with( - OpenProject::Events::STORAGE_TURNED_UNHEALTHY, - storage: storage1, - reason: "custom_error" - ) - end + it "enqueues a job for each automatically managed storage" do + expect { described_class.perform_now }.to change(enqueued_jobs, :count).by(2) end end end diff --git a/modules/team_planner/app/controllers/team_planner/menus_controller.rb b/modules/team_planner/app/controllers/team_planner/menus_controller.rb new file mode 100644 index 000000000000..e782768bac5a --- /dev/null +++ b/modules/team_planner/app/controllers/team_planner/menus_controller.rb @@ -0,0 +1,43 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-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 ::TeamPlanner + class MenusController < ApplicationController + before_action :find_project_by_project_id, + :authorize + + def show + @submenu_menu_items = ::TeamPlanner::Menu.new(project: @project, params:).menu_items + if User.current.allowed_in_project?(:manage_team_planner, @project) && + EnterpriseToken.allows_to?(:team_planner_view) + @create_btn_options = { href: new_project_team_planners_path(@project), module_key: "team_planner" } + end + + render layout: nil + end + end +end diff --git a/modules/team_planner/app/menus/team_planner/menu.rb b/modules/team_planner/app/menus/team_planner/menu.rb new file mode 100644 index 000000000000..7041404b2872 --- /dev/null +++ b/modules/team_planner/app/menus/team_planner/menu.rb @@ -0,0 +1,56 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-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 TeamPlanner + class Menu < Submenu + attr_reader :view_type, :project + + def initialize(project: nil, params: nil) + @view_type = "team_planner" + @project = project + @params = params + + super(view_type:, project:, params:) + end + + def default_queries + [] + end + + def selected?(query_params) + query_params[:id].to_s == params[:id] + end + + def query_params(id) + { id: } + end + + def query_path(query_params) + project_team_planner_path(project, query_params) + end + end +end diff --git a/modules/team_planner/app/views/team_planner/menus/_menu.html.erb b/modules/team_planner/app/views/team_planner/menus/_menu.html.erb new file mode 100644 index 000000000000..7ceb63595fb1 --- /dev/null +++ b/modules/team_planner/app/views/team_planner/menus/_menu.html.erb @@ -0,0 +1,5 @@ + <%= turbo_frame_tag "team_planner_sidemenu", + src: menu_project_team_planners_path(@project, **params.permit(:id)), + target: '_top', + data: { turbo: false }, + loading: :lazy %> diff --git a/modules/team_planner/app/views/team_planner/menus/show.html.erb b/modules/team_planner/app/views/team_planner/menus/show.html.erb new file mode 100644 index 000000000000..7c6fb6985aeb --- /dev/null +++ b/modules/team_planner/app/views/team_planner/menus/show.html.erb @@ -0,0 +1,5 @@ +<%= turbo_frame_tag "team_planner_sidemenu" do %> + <%= render OpenProject::Common::SubmenuComponent.new(sidebar_menu_items: @submenu_menu_items, + searchable: true, + create_btn_options: @create_btn_options) %> +<% end %> diff --git a/modules/team_planner/app/views/team_planner/team_planner/_menu.html.erb b/modules/team_planner/app/views/team_planner/team_planner/_menu.html.erb deleted file mode 100644 index fed603f4a415..000000000000 --- a/modules/team_planner/app/views/team_planner/team_planner/_menu.html.erb +++ /dev/null @@ -1,7 +0,0 @@ - <%= - angular_component_tag 'op-team-planner-sidemenu', - inputs: { - projectId: (@project ? @project.id.to_s : ''), - menuItems: [parent_name, name], - } - %> diff --git a/modules/team_planner/config/routes.rb b/modules/team_planner/config/routes.rb index 2f694e154101..501607c86cc2 100644 --- a/modules/team_planner/config/routes.rb +++ b/modules/team_planner/config/routes.rb @@ -15,6 +15,7 @@ only: %i[index destroy], as: :team_planners do collection do + get "menu" => "team_planner/menus#show" get "/upsale", to: "team_planner/team_planner#upsale", as: :upsale get "/new", to: "team_planner/team_planner#show", as: :new end diff --git a/modules/team_planner/lib/open_project/team_planner/engine.rb b/modules/team_planner/lib/open_project/team_planner/engine.rb index 2a853694ae21..422c42bafe8c 100644 --- a/modules/team_planner/lib/open_project/team_planner/engine.rb +++ b/modules/team_planner/lib/open_project/team_planner/engine.rb @@ -38,7 +38,8 @@ class Engine < ::Rails::Engine settings: {} do project_module :team_planner_view, dependencies: :work_package_tracking, enterprise_feature: true do permission :view_team_planner, - { "team_planner/team_planner": %i[index show upsale overview] }, + { "team_planner/team_planner": %i[index show upsale overview], + "team_planner/menus": %i[show] }, permissible_on: :project, dependencies: %i[view_work_packages], contract_actions: { team_planner: %i[read] } @@ -80,7 +81,7 @@ class Engine < ::Rails::Engine :team_planner_menu, { controller: "/team_planner/team_planner", action: :index }, parent: :team_planner_view, - partial: "team_planner/team_planner/menu", + partial: "team_planner/menus/menu", last: true, caption: :"team_planner.label_team_planner_plural" diff --git a/modules/team_planner/spec/features/query_handling_spec.rb b/modules/team_planner/spec/features/query_handling_spec.rb index 7388ee6f9f8f..fb9d6d312380 100644 --- a/modules/team_planner/spec/features/query_handling_spec.rb +++ b/modules/team_planner/spec/features/query_handling_spec.rb @@ -75,7 +75,7 @@ let(:team_planner) { Pages::TeamPlanner.new project } let(:work_package_page) { Pages::WorkPackagesTable.new project } let(:query_title) { Components::WorkPackages::QueryTitle.new } - let(:query_menu) { Components::WorkPackages::QueryMenu.new } + let(:query_menu) { Components::Submenu.new } let(:filters) { team_planner.filters } current_user { user } @@ -114,8 +114,8 @@ it "shows only team planner queries" do # Go to team planner where no query is shown, only the create option - query_menu.expect_no_menu_entry - expect(page).to have_test_selector("team-planner--create-button") + query_menu.expect_no_items + expect(page).to have_test_selector("team_planner--create-button") # Change filter filters.open @@ -128,11 +128,11 @@ team_planner.expect_and_dismiss_toaster(message: I18n.t("js.notice_successful_create")) # The saved query appears in the side menu... - query_menu.expect_menu_entry "I am your Query" + query_menu.expect_item "I am your Query", selected: true # .. but not in the work packages module work_package_page.visit! - query_menu.expect_menu_entry_not_visible "I am your Query" + query_menu.expect_no_item "I am your Query" end it_behaves_like "module specific query view management" do diff --git a/modules/team_planner/spec/features/team_planner_index_spec.rb b/modules/team_planner/spec/features/team_planner_index_spec.rb index 067fbb09ec3e..f06f50a88c4b 100644 --- a/modules/team_planner/spec/features/team_planner_index_spec.rb +++ b/modules/team_planner/spec/features/team_planner_index_spec.rb @@ -63,7 +63,7 @@ end it "can create an action through the sidebar" do - find_test_selector("team-planner--create-button").click + find_test_selector("team_planner--create-button").click team_planner.expect_no_toaster team_planner.expect_title diff --git a/modules/team_planner/spec/features/team_planner_menu_spec.rb b/modules/team_planner/spec/features/team_planner_menu_spec.rb index 0732a7378215..e7b61a15cabc 100644 --- a/modules/team_planner/spec/features/team_planner_menu_spec.rb +++ b/modules/team_planner/spec/features/team_planner_menu_spec.rb @@ -77,7 +77,7 @@ click_link "Team planners" end - expect(page).not_to have_test_selector("team-planner--create-button") + expect(page).not_to have_test_selector("team_planner--create-button") end end @@ -89,7 +89,7 @@ click_link "Team planners" end - expect(page).not_to have_test_selector("team-planner--create-button") + expect(page).not_to have_test_selector("team_planner--create-button") end end end @@ -105,7 +105,7 @@ click_link "Team planners" end - expect(page).not_to have_test_selector("team-planner--create-button") + expect(page).not_to have_test_selector("team_planner--create-button") end end @@ -117,7 +117,7 @@ click_link "Team planners" end - expect(page).to have_test_selector("team-planner--create-button") + expect(page).to have_test_selector("team_planner--create-button") end end end diff --git a/modules/two_factor_authentication/spec/controllers/two_factor_authentication/authentication_controller_spec.rb b/modules/two_factor_authentication/spec/controllers/two_factor_authentication/authentication_controller_spec.rb index 82208614009b..6edf06bba568 100644 --- a/modules/two_factor_authentication/spec/controllers/two_factor_authentication/authentication_controller_spec.rb +++ b/modules/two_factor_authentication/spec/controllers/two_factor_authentication/authentication_controller_spec.rb @@ -33,7 +33,7 @@ end it "returns a 500" do - expect(response.status).to eq 500 + expect(response).to have_http_status :internal_server_error end end diff --git a/modules/two_factor_authentication/spec/controllers/two_factor_authentication/forced_registration/two_factor_devices_controller_spec.rb b/modules/two_factor_authentication/spec/controllers/two_factor_authentication/forced_registration/two_factor_devices_controller_spec.rb index 2f07ea2454ca..7822efd1f351 100644 --- a/modules/two_factor_authentication/spec/controllers/two_factor_authentication/forced_registration/two_factor_devices_controller_spec.rb +++ b/modules/two_factor_authentication/spec/controllers/two_factor_authentication/forced_registration/two_factor_devices_controller_spec.rb @@ -48,7 +48,7 @@ context "when logged in, but not enabled" do it "does not give access" do - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end end @@ -123,7 +123,7 @@ describe "#get" do it "croaks on missing id" do get :confirm, params: { device_id: 1234 } - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end describe "and registered totp device" do @@ -162,7 +162,7 @@ describe "#post" do it "croaks on missing id" do get :confirm, params: { device_id: 1234 } - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end describe "and registered totp device" do diff --git a/modules/two_factor_authentication/spec/controllers/two_factor_authentication/my/two_factor_devices_controller_spec.rb b/modules/two_factor_authentication/spec/controllers/two_factor_authentication/my/two_factor_devices_controller_spec.rb index 3715283ff620..08560809f191 100644 --- a/modules/two_factor_authentication/spec/controllers/two_factor_authentication/my/two_factor_devices_controller_spec.rb +++ b/modules/two_factor_authentication/spec/controllers/two_factor_authentication/my/two_factor_devices_controller_spec.rb @@ -42,7 +42,7 @@ context "when logged in, but not enabled" do it "does not give access" do - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end end @@ -117,7 +117,7 @@ describe "#get" do it "croaks on missing id" do get :confirm, params: { device_id: 1234 } - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end describe "and registered totp device" do @@ -156,7 +156,7 @@ describe "#post" do it "croaks on missing id" do get :confirm, params: { device_id: 1234 } - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end describe "and registered totp device" do @@ -226,7 +226,7 @@ describe "#destroy" do it "croaks on missing id" do delete :destroy, params: { device_id: "1234" } - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end context "assuming password check is valid" do diff --git a/modules/two_factor_authentication/spec/controllers/two_factor_authentication/users/two_factor_devices_controller_spec.rb b/modules/two_factor_authentication/spec/controllers/two_factor_authentication/users/two_factor_devices_controller_spec.rb index a3765104c703..a843d38dc376 100644 --- a/modules/two_factor_authentication/spec/controllers/two_factor_authentication/users/two_factor_devices_controller_spec.rb +++ b/modules/two_factor_authentication/spec/controllers/two_factor_authentication/users/two_factor_devices_controller_spec.rb @@ -35,7 +35,7 @@ let(:logged_in_user) { other_user } it "does not give access" do - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end end @@ -43,7 +43,7 @@ let(:logged_in_user) { user } it "does not give access" do - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end end @@ -59,7 +59,7 @@ let(:active_strategies) { [] } it "renders a 404 because no strategies enabled" do - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end end end @@ -182,7 +182,7 @@ describe "#destroy" do it "croaks on missing id" do delete :destroy, params: { id: user.id, device_id: "1234" } - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end context "with existing non-default device" do diff --git a/modules/webhooks/spec/controllers/outgoing/admin_controller_spec.rb b/modules/webhooks/spec/controllers/outgoing/admin_controller_spec.rb index 449c04b3c769..0c57a2576a6a 100644 --- a/modules/webhooks/spec/controllers/outgoing/admin_controller_spec.rb +++ b/modules/webhooks/spec/controllers/outgoing/admin_controller_spec.rb @@ -40,7 +40,7 @@ it "renders 403" do get :index - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end end @@ -139,7 +139,7 @@ it "renders 404" do get :edit, params: { webhook_id: "1234" } expect(response).not_to be_successful - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end end end @@ -157,7 +157,7 @@ it "renders an error" do put :update, params: { webhook_id: "bar" } expect(response).not_to be_successful - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end end diff --git a/spec/components/members/role_form_component_spec.rb b/spec/components/members/role_form_component_spec.rb index b2df3d14abd2..bd2b550d566a 100644 --- a/spec/components/members/role_form_component_spec.rb +++ b/spec/components/members/role_form_component_spec.rb @@ -79,7 +79,7 @@ expect(form).to have_css "input[name='member[user_ids][]']", visible: :hidden # rubocop:disable Capybara/SpecificMatcher - expect(form.first("input[name='member[user_ids][]']", visible: :hidden).value).to eq '42' + expect(form.first("input[name='member[user_ids][]']", visible: :hidden).value).to eq "42" end end end diff --git a/spec/components/work_packages/share/user_details_component_spec.rb b/spec/components/shares/user_details_component_spec.rb similarity index 99% rename from spec/components/work_packages/share/user_details_component_spec.rb rename to spec/components/shares/user_details_component_spec.rb index aeb385aa6ca1..5eefdb309e62 100644 --- a/spec/components/work_packages/share/user_details_component_spec.rb +++ b/spec/components/shares/user_details_component_spec.rb @@ -29,7 +29,7 @@ # ++ require "spec_helper" -RSpec.describe WorkPackages::Share::UserDetailsComponent, type: :component do +RSpec.describe Shares::UserDetailsComponent, type: :component do subject { render_inline(described_class.new(share:, manager_mode:, invite_resent:)) } shared_let(:project) { create(:project) } diff --git a/spec/contracts/news/create_contract_spec.rb b/spec/contracts/news/create_contract_spec.rb new file mode 100644 index 000000000000..0c6799f31bb2 --- /dev/null +++ b/spec/contracts/news/create_contract_spec.rb @@ -0,0 +1,39 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require_relative "shared_contract_examples" + +RSpec.describe News::CreateContract do + include_context "ModelContract shared context" + + it_behaves_like "news contract" do + let(:news) { build(:news, title: "Test", project:) } + let(:contract) { described_class.new(news, current_user) } + end +end diff --git a/spec/contracts/news/delete_contract_spec.rb b/spec/contracts/news/delete_contract_spec.rb new file mode 100644 index 000000000000..36c14d0d58af --- /dev/null +++ b/spec/contracts/news/delete_contract_spec.rb @@ -0,0 +1,58 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require "contracts/shared/model_contract_shared_context" + +RSpec.describe News::DeleteContract do + include_context "ModelContract shared context" + + shared_let(:project) { create(:project) } + shared_let(:news) { create(:news, project:) } + + let(:contract) { described_class.new(news, current_user) } + + it_behaves_like "contract is valid for active admins and invalid for regular users" + + context "when user with permission in project" do + let(:current_user) { create(:user, member_with_permissions: { project => %i[manage_news] }) } + + it_behaves_like "contract is valid" + end + + context "when user with permission in other_project" do + let(:other_project) { create(:project) } + let(:current_user) { create(:user, member_with_permissions: { other_project => %i[manage_news] }) } + + it_behaves_like "contract is invalid", base: :error_unauthorized + end + + include_examples "contract reuses the model errors" do + let(:current_user) { build_stubbed(:admin) } + end +end diff --git a/app/components/work_packages/share/invite_user_form_component.rb b/spec/contracts/news/shared_contract_examples.rb similarity index 68% rename from app/components/work_packages/share/invite_user_form_component.rb rename to spec/contracts/news/shared_contract_examples.rb index 945256030394..3417dc4b2ce2 100644 --- a/app/components/work_packages/share/invite_user_form_component.rb +++ b/spec/contracts/news/shared_contract_examples.rb @@ -26,24 +26,21 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module WorkPackages - module Share - class InviteUserFormComponent < ApplicationComponent - include ApplicationHelper - include OpTurbo::Streamable - include OpPrimer::ComponentHelpers - include WorkPackages::Share::Concerns::Authorization +require "spec_helper" +require "contracts/shared/model_contract_shared_context" - def initialize(work_package:, errors: nil) - super +RSpec.shared_examples_for "news contract" do + shared_let(:project) { create(:project) } - @work_package = work_package - @errors = errors - end + context "when user with permission" do + let(:current_user) { create(:user, member_with_permissions: { project => %i[view_news manage_news] }) } - def new_share - @new_share ||= Member.new(entity: @work_package, roles: [Role.new(builtin: Role::BUILTIN_WORK_PACKAGE_VIEWER)]) - end - end + it_behaves_like "contract is valid" + end + + it_behaves_like "contract is valid for active admins and invalid for regular users" + + include_examples "contract reuses the model errors" do + let(:current_user) { build_stubbed(:admin) } end end diff --git a/spec/contracts/news/update_contract_spec.rb b/spec/contracts/news/update_contract_spec.rb new file mode 100644 index 000000000000..c3779aea6b49 --- /dev/null +++ b/spec/contracts/news/update_contract_spec.rb @@ -0,0 +1,39 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require_relative "shared_contract_examples" + +RSpec.describe News::UpdateContract do + include_context "ModelContract shared context" + + it_behaves_like "news contract" do + let(:news) { build_stubbed(:news, project:) } + let(:contract) { described_class.new(news, current_user) } + end +end diff --git a/spec/contracts/queries/projects/project_queries/create_contract_spec.rb b/spec/contracts/queries/projects/project_queries/create_contract_spec.rb index ec9466424cfd..d0a58e0b3aae 100644 --- a/spec/contracts/queries/projects/project_queries/create_contract_spec.rb +++ b/spec/contracts/queries/projects/project_queries/create_contract_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Queries::Projects::ProjectQueries::CreateContract do it_behaves_like "project queries contract" do let(:query) do - Queries::Projects::ProjectQuery.new(name: query_name).tap do |query| + ProjectQuery.new(name: query_name).tap do |query| query.extend(OpenProject::ChangedBySystem) query.change_by_system do diff --git a/spec/contracts/queries/projects/project_queries/loading_contract_spec.rb b/spec/contracts/queries/projects/project_queries/loading_contract_spec.rb index 7afe890ed826..7ffe7bc2af24 100644 --- a/spec/contracts/queries/projects/project_queries/loading_contract_spec.rb +++ b/spec/contracts/queries/projects/project_queries/loading_contract_spec.rb @@ -38,7 +38,7 @@ let(:query_orders) { [%w[name asc]] } let(:query_columns) { ["name", "public"] } let(:query) do - Queries::Projects::ProjectQuery.new do |query| + ProjectQuery.new do |query| query_filters.each do |key, operator, values| query.where(key, operator, values) end diff --git a/spec/contracts/queries/projects/project_queries/update_contract_spec.rb b/spec/contracts/queries/projects/project_queries/update_contract_spec.rb index 384e76874c97..546f12fc8380 100644 --- a/spec/contracts/queries/projects/project_queries/update_contract_spec.rb +++ b/spec/contracts/queries/projects/project_queries/update_contract_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Queries::Projects::ProjectQueries::UpdateContract do it_behaves_like "project queries contract" do let(:query) do - Queries::Projects::ProjectQuery.new(name: query_name).tap do |query| + ProjectQuery.new(name: query_name).tap do |query| query.extend(OpenProject::ChangedBySystem) query.change_by_system do diff --git a/spec/contracts/work_package_members/create_contract_spec.rb b/spec/contracts/shares/work_packages/create_contract_spec.rb similarity index 98% rename from spec/contracts/work_package_members/create_contract_spec.rb rename to spec/contracts/shares/work_packages/create_contract_spec.rb index 291380cb657f..0af0bd377379 100644 --- a/spec/contracts/work_package_members/create_contract_spec.rb +++ b/spec/contracts/shares/work_packages/create_contract_spec.rb @@ -29,7 +29,7 @@ require "spec_helper" require_relative "shared_contract_examples" -RSpec.describe WorkPackageMembers::CreateContract do +RSpec.describe Shares::WorkPackages::CreateContract do it_behaves_like "work package member contract" do let(:member) do Member.new(roles: member_roles, diff --git a/spec/contracts/work_package_members/delete_contract_spec.rb b/spec/contracts/shares/work_packages/delete_contract_spec.rb similarity index 98% rename from spec/contracts/work_package_members/delete_contract_spec.rb rename to spec/contracts/shares/work_packages/delete_contract_spec.rb index c16d829481df..648466cd27d6 100644 --- a/spec/contracts/work_package_members/delete_contract_spec.rb +++ b/spec/contracts/shares/work_packages/delete_contract_spec.rb @@ -29,7 +29,7 @@ require "spec_helper" require "contracts/shared/model_contract_shared_context" -RSpec.describe WorkPackageMembers::DeleteContract do +RSpec.describe Shares::WorkPackages::DeleteContract do include_context "ModelContract shared context" let(:contract) { described_class.new(member, current_user) } diff --git a/spec/contracts/work_package_members/shared_contract_examples.rb b/spec/contracts/shares/work_packages/shared_contract_examples.rb similarity index 100% rename from spec/contracts/work_package_members/shared_contract_examples.rb rename to spec/contracts/shares/work_packages/shared_contract_examples.rb diff --git a/spec/contracts/work_package_members/update_contract_spec.rb b/spec/contracts/shares/work_packages/update_contract_spec.rb similarity index 97% rename from spec/contracts/work_package_members/update_contract_spec.rb rename to spec/contracts/shares/work_packages/update_contract_spec.rb index 707e3b021b11..ae5bad5ee273 100644 --- a/spec/contracts/work_package_members/update_contract_spec.rb +++ b/spec/contracts/shares/work_packages/update_contract_spec.rb @@ -29,7 +29,7 @@ require "spec_helper" require_relative "shared_contract_examples" -RSpec.describe WorkPackageMembers::UpdateContract do +RSpec.describe Shares::WorkPackages::UpdateContract do it_behaves_like "work package member contract" do let(:member) do build_stubbed(:work_package_member, diff --git a/spec/controllers/activities_controller_spec.rb b/spec/controllers/activities_controller_spec.rb index 4c657ca01535..cbbf527eaf64 100644 --- a/spec/controllers/activities_controller_spec.rb +++ b/spec/controllers/activities_controller_spec.rb @@ -30,6 +30,8 @@ RSpec.describe ActivitiesController do shared_let(:admin) { create(:admin) } + shared_let(:project) { create(:project) } + current_user { admin } before do diff --git a/spec/controllers/concerns/authorization_spec.rb b/spec/controllers/concerns/authorization_spec.rb index ac9dbd42f894..63c451dcd6dc 100644 --- a/spec/controllers/concerns/authorization_spec.rb +++ b/spec/controllers/concerns/authorization_spec.rb @@ -28,7 +28,7 @@ require "spec_helper" -RSpec.describe ApplicationController, "enforcement of authorization" do # rubocop:disable RSpec/FilePath, RSpec/SpecFilePathFormat +RSpec.describe ApplicationController, "enforcement of authorization" do # rubocop:disable RSpec/RSpec/SpecFilePathFormat shared_let(:user) { create(:user) } controller_setup = Module.new do diff --git a/spec/controllers/enterprises_controller_spec.rb b/spec/controllers/enterprises_controller_spec.rb index d836668e2c83..305f4ed5e47b 100644 --- a/spec/controllers/enterprises_controller_spec.rb +++ b/spec/controllers/enterprises_controller_spec.rb @@ -181,7 +181,7 @@ end it "renders 404" do - expect(response.status).to eq(404) + expect(response).to have_http_status(:not_found) end end end @@ -195,7 +195,7 @@ end it "is forbidden" do - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end end end diff --git a/spec/controllers/forums_controller_spec.rb b/spec/controllers/forums_controller_spec.rb index b2df0e6b6847..a9304c329e87 100644 --- a/spec/controllers/forums_controller_spec.rb +++ b/spec/controllers/forums_controller_spec.rb @@ -74,7 +74,7 @@ context "when not login_required", with_settings: { login_required: false } do it "renders 404 for not found" do get :index, params: { project_id: "not found" } - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 12857c576280..bd903af6ae8d 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -179,7 +179,7 @@ it "forbids index" do get :index expect(response).not_to be_successful - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end it "shows" do @@ -191,7 +191,7 @@ it "forbids new" do get :new expect(response).not_to be_successful - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end it "forbids create" do @@ -200,14 +200,14 @@ end.not_to(change(Group, :count)) expect(response).not_to be_successful - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end it "forbids edit" do get :edit, params: { id: group.id } expect(response).not_to be_successful - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end end end diff --git a/spec/controllers/homescreen_controller_spec.rb b/spec/controllers/homescreen_controller_spec.rb index 2d1b8eabfad0..92c19e4d53b4 100644 --- a/spec/controllers/homescreen_controller_spec.rb +++ b/spec/controllers/homescreen_controller_spec.rb @@ -46,7 +46,7 @@ shared_examples "renders blocks" do it "renders a response" do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) end describe "with rendered views" do diff --git a/spec/controllers/my_controller_spec.rb b/spec/controllers/my_controller_spec.rb index 627a500e55a6..3b2fc53d5397 100644 --- a/spec/controllers/my_controller_spec.rb +++ b/spec/controllers/my_controller_spec.rb @@ -212,7 +212,7 @@ let!(:user_session) { Sessions::UserSession.find_by(session_id: "internal_foobar") } let(:params) do - { user: { mail: "foo@example.org"} } + { user: { mail: "foo@example.org" } } end it "clears other sessions and removes tokens" do diff --git a/spec/controllers/news_controller_spec.rb b/spec/controllers/news_controller_spec.rb index daebacc36056..803b68ac5b0a 100644 --- a/spec/controllers/news_controller_spec.rb +++ b/spec/controllers/news_controller_spec.rb @@ -33,15 +33,10 @@ include BecomeMember - let(:user) do - create(:admin) - end - let(:project) { create(:project) } let(:news) { create(:news) } - before do - allow(User).to receive(:current).and_return user - end + shared_let(:project) { create(:project) } + shared_current_user { create(:admin) } describe "#index" do it "renders index" do @@ -101,7 +96,7 @@ describe "#create" do context "with news_added notifications" do it "persists a news item" do - become_member(project, user) + become_member(project, current_user) post :create, params: { @@ -117,7 +112,7 @@ news = News.find_by!(title: "NewsControllerTest") expect(news).not_to be_nil expect(news.description).to eq "This is the description" - expect(news.author).to eq user + expect(news.author).to eq current_user expect(news.project).to eq project end end diff --git a/spec/controllers/placeholder_users/memberships_controller_spec.rb b/spec/controllers/placeholder_users/memberships_controller_spec.rb index b4bf36edf6e6..f8e5e28c4c50 100644 --- a/spec/controllers/placeholder_users/memberships_controller_spec.rb +++ b/spec/controllers/placeholder_users/memberships_controller_spec.rb @@ -70,7 +70,7 @@ } } - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end end @@ -81,7 +81,7 @@ id: 1234 } - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end end @@ -92,7 +92,7 @@ id: 1234 } - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end end end @@ -126,7 +126,7 @@ } } - expect(response.status).to eq 302 + expect(response).to have_http_status :found expect(placeholder_user.reload.memberships).to be_empty end end @@ -142,7 +142,7 @@ id: membership.id } - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end end @@ -153,7 +153,7 @@ id: membership.id } - expect(response.status).to eq 404 + expect(response).to have_http_status :not_found end end end diff --git a/spec/controllers/placeholder_users_controller_spec.rb b/spec/controllers/placeholder_users_controller_spec.rb index 401464a45a11..f1c371a40a4f 100644 --- a/spec/controllers/placeholder_users_controller_spec.rb +++ b/spec/controllers/placeholder_users_controller_spec.rb @@ -35,7 +35,7 @@ shared_examples "do not allow non-admins" do it "responds with unauthorized status" do expect(response).not_to be_successful - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end end @@ -348,7 +348,7 @@ it "responds with unauthorized status" do expect(response).not_to be_successful - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end end @@ -359,7 +359,7 @@ it "responds with unauthorized status" do expect(response).not_to be_successful - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end end end diff --git a/spec/controllers/projects/queries_controller_spec.rb b/spec/controllers/projects/queries_controller_spec.rb index a1cce957c2b8..dc8a82ad9f1b 100644 --- a/spec/controllers/projects/queries_controller_spec.rb +++ b/spec/controllers/projects/queries_controller_spec.rb @@ -36,7 +36,7 @@ before do scope = instance_double(ActiveRecord::Relation) - allow(Queries::Projects::ProjectQuery).to receive(:visible).with(user).and_return(scope) + allow(ProjectQuery).to receive(:visible).with(user).and_return(scope) allow(scope).to receive(:find).with(query.id.to_s).and_return(query) login_as user @@ -97,7 +97,7 @@ before do scope = instance_double(ActiveRecord::Relation) - allow(Queries::Projects::ProjectQuery).to receive(:visible).and_return(scope) + allow(ProjectQuery).to receive(:visible).and_return(scope) allow(scope).to receive(:find).with(query_id).and_return(query) login_as user @@ -210,7 +210,7 @@ before do allow(controller).to receive(:permitted_query_params).and_return(query_params) scope = instance_double(ActiveRecord::Relation) - allow(Queries::Projects::ProjectQuery).to receive(:visible).and_return(scope) + allow(ProjectQuery).to receive(:visible).and_return(scope) allow(scope).to receive(:find).with(query_id).and_return(query) allow(service_class).to receive(:new).with(model: query, user:).and_return(service_instance) allow(service_instance).to receive(:call).with(query_params).and_return(service_result) @@ -283,7 +283,7 @@ before do allow(controller).to receive(:permitted_query_params).and_return(query_params) scope = instance_double(ActiveRecord::Relation) - allow(Queries::Projects::ProjectQuery).to receive(:visible).and_return(scope) + allow(ProjectQuery).to receive(:visible).and_return(scope) allow(scope).to receive(:find).with(query_id).and_return(query) allow(service_class).to receive(:new).with(model: query, user:).and_return(service_instance) allow(service_instance).to receive(:call).with(query_params).and_return(service_result) @@ -356,7 +356,7 @@ before do allow(controller).to receive(:permitted_query_params).and_return(query_params) scope = instance_double(ActiveRecord::Relation) - allow(Queries::Projects::ProjectQuery).to receive(:visible).and_return(scope) + allow(ProjectQuery).to receive(:visible).and_return(scope) allow(scope).to receive(:find).with(query_id).and_return(query) allow(service_class).to receive(:new).with(model: query, user:).and_return(service_instance) allow(service_instance).to receive(:call).with(query_params).and_return(service_result) @@ -425,7 +425,7 @@ before do scope = instance_double(ActiveRecord::Relation) - allow(Queries::Projects::ProjectQuery).to receive(:visible).and_return(scope) + allow(ProjectQuery).to receive(:visible).and_return(scope) allow(scope).to receive(:find).with(query_id).and_return(query) allow(service_class).to receive(:new).with(model: query, user:).and_return(service_instance) diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index ec6ae981a623..db43e15d6b68 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -156,7 +156,7 @@ it "shows an error" do get "copy", params: { id: project.id } - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end end end diff --git a/spec/controllers/types_controller_spec.rb b/spec/controllers/types_controller_spec.rb index 88e8a5f7e736..8c8b464ec2c3 100644 --- a/spec/controllers/types_controller_spec.rb +++ b/spec/controllers/types_controller_spec.rb @@ -53,7 +53,7 @@ describe "the access should be restricted" do before { get "index" } - it { expect(response.status).to eq(403) } + it { expect(response).to have_http_status(:forbidden) } end end @@ -61,7 +61,7 @@ describe "the access should be restricted" do before { get "new" } - it { expect(response.status).to eq(403) } + it { expect(response).to have_http_status(:forbidden) } end end @@ -69,7 +69,7 @@ describe "the access should be restricted" do before { get "edit", params: { id: "123" } } - it { expect(response.status).to eq(403) } + it { expect(response).to have_http_status(:forbidden) } end end @@ -77,7 +77,7 @@ describe "the access should be restricted" do before { post "create" } - it { expect(response.status).to eq(403) } + it { expect(response).to have_http_status(:forbidden) } end end @@ -85,7 +85,7 @@ describe "the access should be restricted" do before { delete "destroy", params: { id: "123" } } - it { expect(response.status).to eq(403) } + it { expect(response).to have_http_status(:forbidden) } end end @@ -93,7 +93,7 @@ describe "the access should be restricted" do before { post "update", params: { id: "123" } } - it { expect(response.status).to eq(403) } + it { expect(response).to have_http_status(:forbidden) } end end @@ -101,7 +101,7 @@ describe "the access should be restricted" do before { post "move", params: { id: "123" } } - it { expect(response.status).to eq(403) } + it { expect(response).to have_http_status(:forbidden) } end end end @@ -161,7 +161,7 @@ post :create, params: end - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it "shows an error message" do expect(response.body).to have_content("Name can't be blank") diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 9c590f3b7aad..30719976b887 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -194,7 +194,7 @@ end it "returns 403 forbidden" do - expect(response.status).to eq 403 + expect(response).to have_http_status :forbidden end end @@ -401,7 +401,7 @@ let(:change_action) { :wtf } it "renders 400" do - expect(response.status).to eq(400) + expect(response).to have_http_status(:bad_request) expect(response).not_to render_template "users/change_status_info" end end diff --git a/spec/controllers/versions_controller_spec.rb b/spec/controllers/versions_controller_spec.rb index 38ae203668d3..9883e8ed6fde 100644 --- a/spec/controllers/versions_controller_spec.rb +++ b/spec/controllers/versions_controller_spec.rb @@ -243,7 +243,7 @@ it "renders correctly" do login_as(user) get :new, params: { project_id: project.id } - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) end end diff --git a/spec/controllers/wiki_controller_spec.rb b/spec/controllers/wiki_controller_spec.rb index 9adb49497620..0b0d4822b703 100644 --- a/spec/controllers/wiki_controller_spec.rb +++ b/spec/controllers/wiki_controller_spec.rb @@ -110,7 +110,7 @@ it "renders 404 if used with an unknown page title" do get "new_child", params: { project_id: project, id: "foobar" } - expect(response.status).to eq(404) # not found + expect(response).to have_http_status(:not_found) end end diff --git a/spec/controllers/wiki_menu_authentication_spec.rb b/spec/controllers/wiki_menu_authentication_spec.rb index f67dc5b33bcc..7ec8ec5c3b40 100644 --- a/spec/controllers/wiki_menu_authentication_spec.rb +++ b/spec/controllers/wiki_menu_authentication_spec.rb @@ -62,7 +62,7 @@ get "edit", params: @params - expect(response.status).to eq(403) # forbidden + expect(response).to have_http_status(:forbidden) end end end diff --git a/spec/factories/principal_factory.rb b/spec/factories/principal_factory.rb index 95cf7e36211f..3c59958116d9 100644 --- a/spec/factories/principal_factory.rb +++ b/spec/factories/principal_factory.rb @@ -90,7 +90,7 @@ create(:member, principal:, project: object, roles: Array(role_or_roles)) when WorkPackage create(:member, principal:, entity: object, project: object.project, roles: Array(role_or_roles)) - when Queries::Projects::ProjectQuery + when ProjectQuery create(:member, principal:, entity: object, project: nil, roles: Array(role_or_roles)) end end diff --git a/spec/factories/queries/project_query_factory.rb b/spec/factories/queries/project_query_factory.rb index 969a39389522..d8b9da04e18d 100644 --- a/spec/factories/queries/project_query_factory.rb +++ b/spec/factories/queries/project_query_factory.rb @@ -27,7 +27,7 @@ # ++ FactoryBot.define do - factory :project_query, class: "Queries::Projects::ProjectQuery" do + factory :project_query, class: "ProjectQuery" do sequence(:name) { |n| "Project query #{n}" } public { false } diff --git a/spec/features/admin/custom_fields/link_custom_field_spec.rb b/spec/features/admin/custom_fields/link_custom_field_spec.rb index c5b062959468..69377f473916 100644 --- a/spec/features/admin/custom_fields/link_custom_field_spec.rb +++ b/spec/features/admin/custom_fields/link_custom_field_spec.rb @@ -53,7 +53,7 @@ # Expect field to be created cf = CustomField.last expect(cf.name).to eq("My Link CF") - expect(cf.field_format).to eq 'link' + expect(cf.field_format).to eq "link" # Edit again find("a", text: "My Link CF").click diff --git a/spec/features/admin/project_custom_fields/list_spec.rb b/spec/features/admin/project_custom_fields/list_spec.rb index 2c07ed5e272f..612cd42a6735 100644 --- a/spec/features/admin/project_custom_fields/list_spec.rb +++ b/spec/features/admin/project_custom_fields/list_spec.rb @@ -26,29 +26,29 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require 'spec_helper' -require_relative 'shared_context' +require "spec_helper" +require_relative "shared_context" -RSpec.describe 'List project custom fields', :js do - include_context 'with seeded project custom fields' +RSpec.describe "List project custom fields", :js do + include_context "with seeded project custom fields" - context 'with unsufficient permissions' do - it 'is not accessible' do + context "with unsufficient permissions" do + it "is not accessible" do login_as(non_admin) visit admin_settings_project_custom_fields_path - expect(page).to have_text('You are not authorized to access this page.') + expect(page).to have_text("You are not authorized to access this page.") end end - context 'with sufficient permissions' do + context "with sufficient permissions" do before do login_as(admin) visit admin_settings_project_custom_fields_path end - it 'shows all sections in the correct order and allows reordering via menu or drag and drop' do - containers = page.all('.op-project-custom-field-section-container') + it "shows all sections in the correct order and allows reordering via menu or drag and drop" do + containers = page.all(".op-project-custom-field-section-container") expect(containers[0].text).to include(section_for_input_fields.name) expect(containers[1].text).to include(section_for_select_fields.name) @@ -58,7 +58,7 @@ visit admin_settings_project_custom_fields_path - containers = page.all('.op-project-custom-field-section-container') + containers = page.all(".op-project-custom-field-section-container") expect(containers[0].text).to include(section_for_input_fields.name) expect(containers[1].text).to include(section_for_multi_select_fields.name) @@ -67,9 +67,9 @@ # TODO: Add drag and drop test end - it 'allows to delete a section only if no project custom fields are assigned to it' do + it "allows to delete a section only if no project custom fields are assigned to it" do within_project_custom_field_section_menu(section_for_multi_select_fields) do - expect(page).to have_css("button[aria-disabled='true']", text: 'Delete') + expect(page).to have_css("button[aria-disabled='true']", text: "Delete") end multi_list_project_custom_field.destroy @@ -79,11 +79,11 @@ visit admin_settings_project_custom_fields_path within_project_custom_field_section_menu(section_for_multi_select_fields) do - expect(page).to have_no_css("button[aria-disabled='true']", text: 'Delete') - expect(page).to have_button('Delete') + expect(page).to have_no_css("button[aria-disabled='true']", text: "Delete") + expect(page).to have_button("Delete") accept_confirm do - click_on('Delete') + click_on("Delete") end end @@ -91,42 +91,42 @@ .to have_no_css("[data-test-selector='project-custom-field-section-container-#{section_for_multi_select_fields.id}']") end - it 'allows to edit a section' do + it "allows to edit a section" do within_project_custom_field_section_menu(section_for_input_fields) do - click_on('Edit title') + click_on("Edit title") end - fill_in('project_custom_field_section_name', with: 'Updated section name') + fill_in("project_custom_field_section_name", with: "Updated section name") - click_on('Save') + click_on("Save") expect(page).to have_no_text(section_for_input_fields.name) - expect(page).to have_text('Updated section name') + expect(page).to have_text("Updated section name") end - it 'allows to create a new section' do - within '#settings-project-custom-fields-header-component' do - click_on('dialog-show-project-custom-field-section-dialog') + it "allows to create a new section" do + within "#settings-project-custom-fields-header-component" do + click_on("dialog-show-project-custom-field-section-dialog") end - fill_in('project_custom_field_section_name', with: 'New section name') + fill_in("project_custom_field_section_name", with: "New section name") - click_on('Save') + click_on("Save") - expect(page).to have_text('New section name') + expect(page).to have_text("New section name") - containers = page.all('.op-project-custom-field-section-container') + containers = page.all(".op-project-custom-field-section-container") - expect(containers[0].text).to include('New section name') + expect(containers[0].text).to include("New section name") expect(containers[1].text).to include(section_for_input_fields.name) expect(containers[2].text).to include(section_for_select_fields.name) expect(containers[3].text).to include(section_for_multi_select_fields.name) end - describe 'managing project custom fields' do - it 'shows all custom fields in the correct order within their section and allows reordering via menu or drag and drop' do + describe "managing project custom fields" do + it "shows all custom fields in the correct order within their section and allows reordering via menu or drag and drop" do within_project_custom_field_section_container(section_for_input_fields) do - containers = page.all('.op-project-custom-field-container') + containers = page.all(".op-project-custom-field-container") expect(containers[0].text).to include(boolean_project_custom_field.name) expect(containers[1].text).to include(string_project_custom_field.name) @@ -137,7 +137,7 @@ end within_project_custom_field_section_container(section_for_select_fields) do - containers = page.all('.op-project-custom-field-container') + containers = page.all(".op-project-custom-field-container") expect(containers[0].text).to include(list_project_custom_field.name) expect(containers[1].text).to include(version_project_custom_field.name) @@ -145,7 +145,7 @@ end within_project_custom_field_section_container(section_for_multi_select_fields) do - containers = page.all('.op-project-custom-field-container') + containers = page.all(".op-project-custom-field-container") expect(containers[0].text).to include(multi_list_project_custom_field.name) expect(containers[1].text).to include(multi_version_project_custom_field.name) @@ -157,7 +157,7 @@ visit admin_settings_project_custom_fields_path within_project_custom_field_section_container(section_for_multi_select_fields) do - containers = page.all('.op-project-custom-field-container') + containers = page.all(".op-project-custom-field-container") expect(containers[0].text).to include(multi_list_project_custom_field.name) expect(containers[1].text).to include(multi_user_project_custom_field.name) @@ -167,9 +167,9 @@ # TODO: Add drag and drop test end - it 'shows the number of projects using a custom field' do + it "shows the number of projects using a custom field" do within_project_custom_field_container(boolean_project_custom_field) do - expect(page).to have_text('0 Projects') + expect(page).to have_text("0 Projects") end project = create(:project) @@ -178,29 +178,29 @@ visit admin_settings_project_custom_fields_path within_project_custom_field_container(boolean_project_custom_field) do - expect(page).to have_text('1 Project') + expect(page).to have_text("1 Project") end end - it 'allows to delete a custom field' do + it "allows to delete a custom field" do within_project_custom_field_menu(boolean_project_custom_field) do accept_confirm do - click_on('Delete') + click_on("Delete") end end expect(page).to have_no_css("[data-test-selector='project-custom-field-container-#{boolean_project_custom_field.id}']") end - it 'redirects to the custom field edit page via menu item' do + it "redirects to the custom field edit page via menu item" do within_project_custom_field_menu(boolean_project_custom_field) do - click_on('Edit') + click_on("Edit") end expect(page).to have_current_path(edit_admin_settings_project_custom_field_path(boolean_project_custom_field)) end - it 'redirects to the custom field edit page via click on the name of the custom field' do + it "redirects to the custom field edit page via click on the name of the custom field" do within_project_custom_field_container(boolean_project_custom_field) do click_on(boolean_project_custom_field.name) end @@ -208,13 +208,13 @@ expect(page).to have_current_path(edit_admin_settings_project_custom_field_path(boolean_project_custom_field)) end - it 'redirects to the custom field new page via header menu button' do + it "redirects to the custom field new page via header menu button" do page.find("[data-test-selector='new-project-custom-field-button']").click - expect(page).to have_current_path(new_admin_settings_project_custom_field_path(type: 'ProjectCustomField')) + expect(page).to have_current_path(new_admin_settings_project_custom_field_path(type: "ProjectCustomField")) end - it 'redirects to the custom field new page via button in empty sections' do + it "redirects to the custom field new page via button in empty sections" do within_project_custom_field_section_container(section_for_multi_select_fields) do expect(page).to have_no_css("[data-test-selector='new-project-custom-field-button']") end @@ -230,7 +230,7 @@ end expect(page).to have_current_path(new_admin_settings_project_custom_field_path( - type: 'ProjectCustomField', + type: "ProjectCustomField", custom_field_section_id: section_for_multi_select_fields.id )) end @@ -246,7 +246,7 @@ def within_project_custom_field_section_container(section, &block) def within_project_custom_field_section_menu(section, &block) within_project_custom_field_section_container(section) do page.find("[data-test-selector='project-custom-field-section-action-menu']").click - within('anchored-position', &block) + within("anchored-position", &block) end end @@ -264,7 +264,7 @@ def within_project_custom_field_container(custom_field, &block) def within_project_custom_field_menu(section, &block) within_project_custom_field_container(section) do page.find("[data-test-selector='project-custom-field-action-menu']").click - within('anchored-position', &block) + within("anchored-position", &block) end end diff --git a/spec/features/admin/project_custom_fields/project_mappings_spec.rb b/spec/features/admin/project_custom_fields/project_mappings_spec.rb index 1f367fb5fcfb..22d5339a287f 100644 --- a/spec/features/admin/project_custom_fields/project_mappings_spec.rb +++ b/spec/features/admin/project_custom_fields/project_mappings_spec.rb @@ -86,6 +86,18 @@ end end + it "shows an error in the dialog when no project is selected before adding" do + create(:project) + expect(page).to have_no_css("dialog") + click_on "Add projects" + + page.within("dialog") do + click_on "Add" + + expect(page).to have_text("Please select a project.") + end + end + it "allows linking a project to a custom field" do project = create(:project) subproject = create(:project, parent: project) diff --git a/spec/features/custom_fields/create_long_text_spec.rb b/spec/features/custom_fields/create_long_text_spec.rb index c05c405168b6..8aee873dcc70 100644 --- a/spec/features/custom_fields/create_long_text_spec.rb +++ b/spec/features/custom_fields/create_long_text_spec.rb @@ -6,7 +6,7 @@ let(:cf_page) { Pages::CustomFields.new } let(:editor) { Components::WysiwygEditor.new "#custom_field_form" } let(:type) { create(:type_task) } - let(:project) { create(:project, enabled_module_names: %i[work_package_tracking], types: [type]) } + let!(:project) { create(:project, enabled_module_names: %i[work_package_tracking], types: [type]) } let(:wp_page) { Pages::FullWorkPackageCreate.new project: } diff --git a/spec/features/news/global_menu_item_spec.rb b/spec/features/news/global_menu_item_spec.rb index 10dd02d67c2f..44de7333c072 100644 --- a/spec/features/news/global_menu_item_spec.rb +++ b/spec/features/news/global_menu_item_spec.rb @@ -34,6 +34,7 @@ RSpec.describe "News global menu item spec", :js, :with_cuprite do shared_let(:admin) { create(:admin) } shared_let(:user_without_permissions) { create(:user) } + shared_let(:project) { create(:project) } before do login_as current_user diff --git a/spec/features/projects/favorite_spec.rb b/spec/features/projects/favorite_spec.rb index bc581a19456c..d3c47dd7f647 100644 --- a/spec/features/projects/favorite_spec.rb +++ b/spec/features/projects/favorite_spec.rb @@ -44,6 +44,7 @@ let(:my_page) do Pages::My::Page.new end + context "as a user" do before do login_as user diff --git a/spec/features/projects/global_menu_item_spec.rb b/spec/features/projects/global_menu_item_spec.rb index 7bf2ab370209..e15f6f50d7e1 100644 --- a/spec/features/projects/global_menu_item_spec.rb +++ b/spec/features/projects/global_menu_item_spec.rb @@ -32,10 +32,12 @@ require "spec_helper" RSpec.describe "Projects global menu item", :js, :with_cuprite do - let(:user) { create(:user) } + shared_let(:user) { create(:user) } + shared_let(:admin) { create(:admin) } + + current_user { user } before do - login_as user visit root_path end @@ -65,7 +67,7 @@ end context "with an admin user" do - let(:user) { create(:admin) } + current_user { admin } it "renders the archived filter as well" do within "#main-menu" do diff --git a/spec/features/projects/project_custom_fields/overview_page/dialog/permission_spec.rb b/spec/features/projects/project_custom_fields/overview_page/dialog/permission_spec.rb index aec9e99269a7..b8a2d54f196e 100644 --- a/spec/features/projects/project_custom_fields/overview_page/dialog/permission_spec.rb +++ b/spec/features/projects/project_custom_fields/overview_page/dialog/permission_spec.rb @@ -26,15 +26,15 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require 'spec_helper' -require_relative '../shared_context' +require "spec_helper" +require_relative "../shared_context" -RSpec.describe 'Edit project custom fields on project overview page', :js do - include_context 'with seeded projects, members and project custom fields' +RSpec.describe "Edit project custom fields on project overview page", :js do + include_context "with seeded projects, members and project custom fields" let(:overview_page) { Pages::Projects::Show.new(project) } - describe 'with insufficient permissions' do + describe "with insufficient permissions" do # turboframe sidebar request is covered by a controller spec checking for 403 # async dialog content request is be covered by a controller spec checking for 403 # via spec/permissions/manage_project_custom_values_spec.rb @@ -43,20 +43,20 @@ overview_page.visit_page end - it 'does not show the edit buttons' do + it "does not show the edit buttons" do overview_page.within_async_loaded_sidebar do expect(page).to have_no_css("[data-test-selector='project-custom-field-section-edit-button']") end end end - describe 'with sufficient permissions' do + describe "with sufficient permissions" do before do login_as member_with_project_edit_permissions overview_page.visit_page end - it 'shows the edit buttons' do + it "shows the edit buttons" do overview_page.within_async_loaded_sidebar do expect(page).to have_css("[data-test-selector='project-custom-field-section-edit-button']", count: 3) end diff --git a/spec/features/projects/project_custom_fields/settings/mapping_spec.rb b/spec/features/projects/project_custom_fields/settings/mapping_spec.rb index d30b14b7f698..940c4c02bb89 100644 --- a/spec/features/projects/project_custom_fields/settings/mapping_spec.rb +++ b/spec/features/projects/project_custom_fields/settings/mapping_spec.rb @@ -26,16 +26,16 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require 'spec_helper' +require "spec_helper" -RSpec.describe 'Projects custom fields mapping via project settings', :js, :with_cuprite do - let(:project) { create(:project, name: 'Foo project', identifier: 'foo-project') } - let(:other_project) { create(:project, name: 'Bar project', identifier: 'bar-project') } +RSpec.describe "Projects custom fields mapping via project settings", :js, :with_cuprite do + let(:project) { create(:project, name: "Foo project", identifier: "foo-project") } + let(:other_project) { create(:project, name: "Bar project", identifier: "bar-project") } let!(:user_with_sufficient_permissions) do create(:user, - firstname: 'Project', - lastname: 'Admin', + firstname: "Project", + lastname: "Admin", member_with_permissions: { project => %w[ view_work_packages @@ -52,8 +52,8 @@ let!(:member_in_project) do create(:user, - firstname: 'Member 1', - lastname: 'In Project', + firstname: "Member 1", + lastname: "In Project", member_with_permissions: { project => %w[ edit_project view_work_packages @@ -62,69 +62,69 @@ let!(:another_member_in_project) do create(:user, - firstname: 'Member 2', - lastname: 'In Project', + firstname: "Member 2", + lastname: "In Project", member_with_permissions: { project => %w[ view_work_packages ] }) end - let!(:section_for_input_fields) { create(:project_custom_field_section, name: 'Input fields') } - let!(:section_for_select_fields) { create(:project_custom_field_section, name: 'Select fields') } - let!(:section_for_multi_select_fields) { create(:project_custom_field_section, name: 'Multi select fields') } + let!(:section_for_input_fields) { create(:project_custom_field_section, name: "Input fields") } + let!(:section_for_select_fields) { create(:project_custom_field_section, name: "Select fields") } + let!(:section_for_multi_select_fields) { create(:project_custom_field_section, name: "Multi select fields") } let!(:boolean_project_custom_field) do - create(:boolean_project_custom_field, name: 'Boolean field', + create(:boolean_project_custom_field, name: "Boolean field", project_custom_field_section: section_for_input_fields) end let!(:string_project_custom_field) do - create(:string_project_custom_field, name: 'String field', + create(:string_project_custom_field, name: "String field", project_custom_field_section: section_for_input_fields) end let!(:list_project_custom_field) do - create(:list_project_custom_field, name: 'List field', + create(:list_project_custom_field, name: "List field", project_custom_field_section: section_for_select_fields, - possible_values: ['Option 1', 'Option 2', 'Option 3']) + possible_values: ["Option 1", "Option 2", "Option 3"]) end let!(:multi_list_project_custom_field) do - create(:list_project_custom_field, name: 'Multi list field', + create(:list_project_custom_field, name: "Multi list field", project_custom_field_section: section_for_multi_select_fields, - possible_values: ['Option 1', 'Option 2', 'Option 3'], + possible_values: ["Option 1", "Option 2", "Option 3"], multi_value: true) end - describe 'with insufficient permissions' do + describe "with insufficient permissions" do before do login_as member_in_project # can edit project but is not allowed to select project custom fields end - it 'does not show the menu entry in the project settings menu' do + it "does not show the menu entry in the project settings menu" do visit project_settings_general_path(project) - within '#menu-sidebar' do + within "#menu-sidebar" do expect(page).to have_no_css("li[data-name='settings_project_custom_fields']") end end - it 'does not show the project custom fields page' do + it "does not show the project custom fields page" do visit project_settings_project_custom_fields_path(project) - expect(page).to have_content('You are not authorized to access this page.') + expect(page).to have_content("You are not authorized to access this page.") end end - describe 'with sufficient permissions' do + describe "with sufficient permissions" do before do login_as user_with_sufficient_permissions end - it 'does show the menu entry in the project settings menu' do + it "does show the menu entry in the project settings menu" do visit project_settings_general_path(project) - within '#menu-sidebar' do + within "#menu-sidebar" do expect(page).to have_css("li[data-name='settings_project_custom_fields']") end end @@ -151,17 +151,17 @@ expect(page).to have_content("Multi list field") end - it 'shows all available project custom fields with their correct mapping state' do + it "shows all available project custom fields with their correct mapping state" do visit project_settings_project_custom_fields_path(project) within_custom_field_section_container(section_for_input_fields) do within_custom_field_container(boolean_project_custom_field) do - expect(page).to have_content('Boolean field') + expect(page).to have_content("Boolean field") expect_type("Bool") expect_unchecked_state end within_custom_field_container(string_project_custom_field) do - expect(page).to have_content('String field') + expect(page).to have_content("String field") expect_type("String") expect_unchecked_state end @@ -169,7 +169,7 @@ within_custom_field_section_container(section_for_select_fields) do within_custom_field_container(list_project_custom_field) do - expect(page).to have_content('List field') + expect(page).to have_content("List field") expect_type("List") expect_unchecked_state end @@ -177,14 +177,14 @@ within_custom_field_section_container(section_for_multi_select_fields) do within_custom_field_container(multi_list_project_custom_field) do - expect(page).to have_content('Multi list field') + expect(page).to have_content("Multi list field") expect_type("List") expect_unchecked_state end end end - it 'toggles the mapping state of a project custom field for a specific project when clicked' do + it "toggles the mapping state of a project custom field for a specific project when clicked" do visit project_settings_project_custom_fields_path(project) within_custom_field_section_container(section_for_input_fields) do @@ -214,7 +214,7 @@ end end - it 'enables all mapping states of a section for a specific project when bulk action button clicked' do + it "enables all mapping states of a section for a specific project when bulk action button clicked" do visit project_settings_project_custom_fields_path(project) within_custom_field_section_container(section_for_input_fields) do @@ -241,7 +241,7 @@ end end - it 'disables all mapping states of a section for a specific project when bulk action button clicked' do + it "disables all mapping states of a section for a specific project when bulk action button clicked" do visit project_settings_project_custom_fields_path(project) within_custom_field_section_container(section_for_input_fields) do @@ -279,59 +279,59 @@ end end - it 'filters the project custom fields by name with given user input' do + it "filters the project custom fields by name with given user input" do visit project_settings_project_custom_fields_path(project) - fill_in 'project-custom-fields-mapping-filter', with: 'Boolean' + fill_in "project-custom-fields-mapping-filter", with: "Boolean" within_custom_field_section_container(section_for_input_fields) do - expect(page).to have_content('Boolean field') - expect(page).to have_no_content('String field') + expect(page).to have_content("Boolean field") + expect(page).to have_no_content("String field") end within_custom_field_section_container(section_for_select_fields) do - expect(page).to have_no_content('List field') + expect(page).to have_no_content("List field") end within_custom_field_section_container(section_for_multi_select_fields) do - expect(page).to have_no_content('Multi list field') + expect(page).to have_no_content("Multi list field") end end - it 'shows the project custom field sections in the correct order' do + it "shows the project custom field sections in the correct order" do visit project_settings_project_custom_fields_path(project) - sections = page.all('.op-project-custom-field-section') + sections = page.all(".op-project-custom-field-section") expect(sections.size).to eq(3) - expect(sections[0].text).to include('Input fields') - expect(sections[1].text).to include('Select fields') - expect(sections[2].text).to include('Multi select fields') + expect(sections[0].text).to include("Input fields") + expect(sections[1].text).to include("Select fields") + expect(sections[2].text).to include("Multi select fields") section_for_input_fields.move_to_bottom visit project_settings_project_custom_fields_path(project) - sections = page.all('.op-project-custom-field-section') + sections = page.all(".op-project-custom-field-section") expect(sections.size).to eq(3) - expect(sections[0].text).to include('Select fields') - expect(sections[1].text).to include('Multi select fields') - expect(sections[2].text).to include('Input fields') + expect(sections[0].text).to include("Select fields") + expect(sections[1].text).to include("Multi select fields") + expect(sections[2].text).to include("Input fields") end - it 'shows the project custom fields in the correct order within the sections' do + it "shows the project custom fields in the correct order within the sections" do visit project_settings_project_custom_fields_path(project) within_custom_field_section_container(section_for_input_fields) do - custom_fields = page.all('.op-project-custom-field') + custom_fields = page.all(".op-project-custom-field") expect(custom_fields.size).to eq(2) - expect(custom_fields[0].text).to include('Boolean field') - expect(custom_fields[1].text).to include('String field') + expect(custom_fields[0].text).to include("Boolean field") + expect(custom_fields[1].text).to include("String field") end boolean_project_custom_field.move_to_bottom @@ -339,21 +339,21 @@ visit project_settings_project_custom_fields_path(project) within_custom_field_section_container(section_for_input_fields) do - custom_fields = page.all('.op-project-custom-field') + custom_fields = page.all(".op-project-custom-field") expect(custom_fields.size).to eq(2) - expect(custom_fields[0].text).to include('String field') - expect(custom_fields[1].text).to include('Boolean field') + expect(custom_fields[0].text).to include("String field") + expect(custom_fields[1].text).to include("Boolean field") end end - context 'with visibility of project custom fields' do - let!(:section_with_invisible_fields) { create(:project_custom_field_section, name: 'Section with invisible fields') } + context "with visibility of project custom fields" do + let!(:section_with_invisible_fields) { create(:project_custom_field_section, name: "Section with invisible fields") } let!(:visible_project_custom_field) do create(:project_custom_field, - name: 'Normal field', + name: "Normal field", visible: true, projects: [project], project_custom_field_section: section_with_invisible_fields) @@ -361,13 +361,13 @@ let!(:invisible_project_custom_field) do create(:project_custom_field, - name: 'Admin only field', + name: "Admin only field", visible: false, projects: [project], project_custom_field_section: section_with_invisible_fields) end - context 'with admin permissions' do + context "with admin permissions" do let!(:admin) do create(:admin) end @@ -377,14 +377,14 @@ visit project_settings_project_custom_fields_path(project) end - it 'shows the invisible project custom fields' do + it "shows the invisible project custom fields" do within_custom_field_section_container(section_with_invisible_fields) do - expect(page).to have_content('Normal field') - expect(page).to have_content('Admin only field') + expect(page).to have_content("Normal field") + expect(page).to have_content("Admin only field") end end - it 'includeds the invisible project custom fields in the bulk actions' do + it "includeds the invisible project custom fields in the bulk actions" do within_custom_field_section_container(section_with_invisible_fields) do page .find("[data-test-selector='disable-all-project-custom-field-mappings-#{section_with_invisible_fields.id}']") @@ -411,20 +411,20 @@ end end - context 'with non-admin permissions' do + context "with non-admin permissions" do before do login_as user_with_sufficient_permissions visit project_settings_project_custom_fields_path(project) end - it 'does not show the invisible project custom fields' do + it "does not show the invisible project custom fields" do within_custom_field_section_container(section_with_invisible_fields) do - expect(page).to have_content('Normal field') - expect(page).to have_no_content('Admin only field') + expect(page).to have_content("Normal field") + expect(page).to have_no_content("Admin only field") end end - it 'does not include the invisible project custom fields in the bulk actions' do + it "does not include the invisible project custom fields in the bulk actions" do within_custom_field_section_container(section_with_invisible_fields) do page .find("[data-test-selector='disable-all-project-custom-field-mappings-#{section_with_invisible_fields.id}']") @@ -461,11 +461,11 @@ def expect_type(type) end def expect_checked_state - expect(page).to have_css('.ToggleSwitch-statusOn') + expect(page).to have_css(".ToggleSwitch-statusOn") end def expect_unchecked_state - expect(page).to have_css('.ToggleSwitch-statusOff') + expect(page).to have_css(".ToggleSwitch-statusOff") end def within_custom_field_section_container(section, &) diff --git a/spec/features/types/form_configuration_spec.rb b/spec/features/types/form_configuration_spec.rb index fd4751c28241..1dd212d4d2e0 100644 --- a/spec/features/types/form_configuration_spec.rb +++ b/spec/features/types/form_configuration_spec.rb @@ -32,7 +32,7 @@ shared_let(:admin) { create(:admin) } let(:type) { create(:type) } - let(:project) { create(:project, types: [type]) } + let!(:project) { create(:project, types: [type]) } let(:category) { create(:category, project:) } let(:work_package) do create(:work_package, diff --git a/spec/features/views/shared_examples.rb b/spec/features/views/shared_examples.rb index 36a204707702..a6a328f313cb 100644 --- a/spec/features/views/shared_examples.rb +++ b/spec/features/views/shared_examples.rb @@ -44,7 +44,7 @@ settings_menu.open_and_save_query "My first query" query_title.expect_not_changed query_title.expect_title "My first query" - query_menu.expect_item "My first query" + query_menu.expect_item "My first query", selected: true # Change the filter again filters.add_filter_by "% Complete", "is", ["25"], "percentageDone" @@ -55,7 +55,7 @@ settings_menu.open_and_save_query_as "My second query" query_title.expect_not_changed query_title.expect_title "My second query" - query_menu.expect_item "My second query" + query_menu.expect_item "My second query", selected: true query_menu.expect_item "My first query" # Rename a query @@ -67,7 +67,7 @@ query_title.expect_not_changed query_title.expect_title "My second query (renamed)" - query_menu.expect_item "My second query (renamed)" + query_menu.expect_item "My second query (renamed)", selected: true query_menu.expect_item "My first query" # Delete a query diff --git a/spec/features/work_packages/export_spec.rb b/spec/features/work_packages/export_spec.rb index 74a44afc57f2..a12ca3b80793 100644 --- a/spec/features/work_packages/export_spec.rb +++ b/spec/features/work_packages/export_spec.rb @@ -234,15 +234,16 @@ def export!(expect_success = true) allow_any_instance_of(WorkPackage::PDFExport::WorkPackageListToPdf) .to receive(:export!) .and_return( - ::Exports::Result.new format: :pdf, - title: 'foo', - content: Tempfile.new("something"), - mime_type: "application/pdf" + Exports::Result.new(format: :pdf, + title: "foo", + content: Tempfile.new("something"), + mime_type: "application/pdf") ) - end + end context "table" do let(:export_type) { I18n.t("export.format.pdf_overview_table") } + context "with many columns" do before do # Despite attempts to provoke the error by having a lot of columns, the pdf @@ -292,7 +293,7 @@ def export!(expect_success = true) wp_table.visit_query query work_packages_page.ensure_loaded settings_menu.open_and_choose "Export" - expect(page).not_to have_content(export_type) + expect(page).to have_no_content(export_type) end end @@ -303,7 +304,7 @@ def export!(expect_success = true) it "has no gantt export" do wp_table.visit_query query_tl settings_menu.open_and_choose "Export" - expect(page).not_to have_content(export_type) + expect(page).to have_no_content(export_type) end end diff --git a/spec/features/work_packages/navigation_spec.rb b/spec/features/work_packages/navigation_spec.rb index 6364eed3f47c..3375e552ec36 100644 --- a/spec/features/work_packages/navigation_spec.rb +++ b/spec/features/work_packages/navigation_spec.rb @@ -259,7 +259,7 @@ visit "/projects/#{project.identifier}/work_packages?#{url_query}" wp_table.expect_toast message: "Your view is erroneous and could not be processed.", type: :error - expect(page).to have_css "li", text: "Bad request: id is invalid" + expect(page).to have_css "li", text: "The requested resource could not be found" end end end diff --git a/spec/features/work_packages/share/access_spec.rb b/spec/features/work_packages/share/access_spec.rb index 05e035ec6d0e..d4f00be01dca 100644 --- a/spec/features/work_packages/share/access_spec.rb +++ b/spec/features/work_packages/share/access_spec.rb @@ -32,6 +32,7 @@ :js, :with_cuprite, with_ee: %i[work_package_sharing] do shared_let(:project) { create(:project_with_types) } + shared_let(:int_project_custom_field) { create(:integer_project_custom_field, projects: [project]) } shared_let(:work_package) { create(:work_package, project:, journal_notes: "Hello!") } shared_let(:sharer) { create(:admin) } shared_let(:shared_with_user) { create(:user, firstname: "Mean", lastname: "Turkey") } @@ -87,6 +88,12 @@ # 3. Visiting the Project's URL directly project_page.visit! + # The project overview page is loaded and e.g. custom fields can be seen + # This ensures that the page is loaded. + project_page.within_async_loaded_sidebar do + expect(page).to have_content(int_project_custom_field.name) + end + # # Work Package is now visible project_page.within_sidebar do @@ -161,6 +168,12 @@ # 3. Visiting the Project's URL directly project_page.visit! + # The project overview page is loaded and e.g. custom fields can be seen + # This ensures that the page is loaded. + project_page.within_async_loaded_sidebar do + expect(page).to have_content(int_project_custom_field.name) + end + # # Work Package is now visible project_page.within_sidebar do @@ -240,6 +253,12 @@ # 3. Visiting the Project's URL directly project_page.visit! + # The project overview page is loaded and e.g. custom fields can be seen + # This ensures that the page is loaded. + project_page.within_async_loaded_sidebar do + expect(page).to have_content(int_project_custom_field.name) + end + # # Work Package is now visible project_page.within_sidebar do diff --git a/spec/features/work_packages/share/filter_spec.rb b/spec/features/work_packages/share/filter_spec.rb index c6e7e81e6a12..741725934eea 100644 --- a/spec/features/work_packages/share/filter_spec.rb +++ b/spec/features/work_packages/share/filter_spec.rb @@ -86,7 +86,7 @@ share_modal.expect_shared_count_of(6) # Filter for: project members (users only) - share_modal.filter("type", I18n.t("work_package.sharing.filter.project_member")) + share_modal.filter("type", I18n.t("sharing.filter.project_member")) share_modal.expect_shared_count_of(3) share_modal.expect_shared_with(project_user, "View") @@ -98,7 +98,7 @@ share_modal.expect_not_shared_with(shared_non_project_group) # Filter for: non-project members (users only) - share_modal.filter("type", I18n.t("work_package.sharing.filter.not_project_member")) + share_modal.filter("type", I18n.t("sharing.filter.not_project_member")) share_modal.expect_shared_count_of(1) share_modal.expect_shared_with(non_project_user, "Edit") @@ -109,7 +109,7 @@ share_modal.expect_not_shared_with(shared_non_project_group) # Filter for: project members (groups only) - share_modal.filter("type", I18n.t("work_package.sharing.filter.project_group")) + share_modal.filter("type", I18n.t("sharing.filter.project_group")) share_modal.expect_shared_count_of(1) share_modal.expect_shared_with(shared_project_group, "Edit") @@ -120,7 +120,7 @@ share_modal.expect_not_shared_with(shared_non_project_group) # Filter for: non-project members (groups only) - share_modal.filter("type", I18n.t("work_package.sharing.filter.not_project_group")) + share_modal.filter("type", I18n.t("sharing.filter.not_project_group")) share_modal.expect_shared_count_of(1) share_modal.expect_shared_with(shared_non_project_group, "View") @@ -131,7 +131,7 @@ share_modal.expect_not_shared_with(shared_project_group) # Clicking again on the filter will reset it - share_modal.filter("type", I18n.t("work_package.sharing.filter.not_project_group")) + share_modal.filter("type", I18n.t("sharing.filter.not_project_group")) share_modal.expect_shared_count_of(6) share_modal.expect_shared_with(project_user, "View") @@ -147,7 +147,7 @@ share_modal.expect_shared_count_of(6) # Filter for: all principals with Edit permission - share_modal.filter("role", I18n.t("work_package.sharing.permissions.edit")) + share_modal.filter("role", I18n.t("work_package.permissions.edit")) share_modal.expect_shared_count_of(3) share_modal.expect_shared_with(inherited_project_user, "Edit") @@ -158,7 +158,7 @@ share_modal.expect_not_shared_with(shared_non_project_group) # Filter for: all principals with View permission - share_modal.filter("role", I18n.t("work_package.sharing.permissions.view")) + share_modal.filter("role", I18n.t("work_package.permissions.view")) share_modal.expect_shared_count_of(2) share_modal.expect_shared_with(project_user, "View") @@ -169,7 +169,7 @@ share_modal.expect_not_shared_with(shared_project_group) # Filter for: all principals with Comment permission - share_modal.filter("role", I18n.t("work_package.sharing.permissions.comment")) + share_modal.filter("role", I18n.t("work_package.permissions.comment")) share_modal.expect_shared_count_of(1) share_modal.expect_shared_with(project_user2, "Comment") @@ -180,7 +180,7 @@ share_modal.expect_not_shared_with(shared_non_project_group) # Clicking again on the filter will reset it - share_modal.filter("role", I18n.t("work_package.sharing.permissions.comment")) + share_modal.filter("role", I18n.t("work_package.permissions.comment")) share_modal.expect_shared_count_of(6) share_modal.expect_shared_with(project_user, "View") @@ -198,7 +198,7 @@ # Filter for: all principals with View permission # role: view # type: none - share_modal.filter("role", I18n.t("work_package.sharing.permissions.view")) + share_modal.filter("role", I18n.t("work_package.permissions.view")) share_modal.expect_shared_count_of(2) share_modal.expect_shared_with(project_user, "View") @@ -211,7 +211,7 @@ # Additional filter for: project members (users only) # role: view # type: project members (users only) - share_modal.filter("type", I18n.t("work_package.sharing.filter.project_member")) + share_modal.filter("type", I18n.t("sharing.filter.project_member")) share_modal.expect_shared_count_of(1) share_modal.expect_shared_with(project_user, "View") @@ -224,7 +224,7 @@ # Change type filter to: project members (groups only) # role: view # type: non-project members (groups only) - share_modal.filter("type", I18n.t("work_package.sharing.filter.not_project_group")) + share_modal.filter("type", I18n.t("sharing.filter.not_project_group")) share_modal.expect_shared_count_of(1) share_modal.expect_shared_with(shared_non_project_group, "View") @@ -237,7 +237,7 @@ # Reset role filter # role: none # type: non-project members (groups only) - share_modal.filter("role", I18n.t("work_package.sharing.permissions.view")) + share_modal.filter("role", I18n.t("work_package.permissions.view")) share_modal.expect_shared_count_of(1) share_modal.expect_shared_with(shared_non_project_group, "View") @@ -250,7 +250,7 @@ # Reset type filter # role: none # type: none - share_modal.filter("type", I18n.t("work_package.sharing.filter.not_project_group")) + share_modal.filter("type", I18n.t("sharing.filter.not_project_group")) share_modal.expect_shared_count_of(6) share_modal.expect_shared_with(project_user, "View") @@ -264,8 +264,8 @@ context "and there are no matching results for my filter" do it 'does not check the "toggle all" checkbox' do share_modal.expect_open - share_modal.filter("type", I18n.t("work_package.sharing.filter.not_project_member")) - share_modal.filter("role", I18n.t("work_package.sharing.permissions.view")) + share_modal.filter("type", I18n.t("sharing.filter.not_project_member")) + share_modal.filter("role", I18n.t("work_package.permissions.view")) share_modal.expect_empty_search_blankslate share_modal.expect_shared_count_of(0) diff --git a/spec/features/work_packages/share/share_account_activation_spec.rb b/spec/features/work_packages/share/share_account_activation_spec.rb index abbadf5e2041..be19c842df6d 100644 --- a/spec/features/work_packages/share/share_account_activation_spec.rb +++ b/spec/features/work_packages/share/share_account_activation_spec.rb @@ -33,6 +33,8 @@ RSpec.describe "Work package sharing invited users", :js, :with_cuprite, with_ee: %i[work_package_sharing] do + shared_let(:edit_work_package_role) { create(:edit_work_package_role) } + shared_let(:comment_work_package_role) { create(:comment_work_package_role) } shared_let(:view_work_package_role) { create(:view_work_package_role) } shared_let(:editor) { create(:admin, firstname: "Mr.", lastname: "Sharer") } diff --git a/spec/features/work_packages/share/share_spec.rb b/spec/features/work_packages/share/share_spec.rb index 33d4fbc47b7b..2151b4fad217 100644 --- a/spec/features/work_packages/share/share_spec.rb +++ b/spec/features/work_packages/share/share_spec.rb @@ -450,7 +450,7 @@ def inherited_member_roles(group:) # The number of shared people has not changed, but an error message is shown share_modal.expect_shared_count_of(6) - share_modal.expect_error_message(I18n.t("work_package.sharing.warning_locked_user", user: locked_user.name)) + share_modal.expect_error_message(I18n.t("sharing.warning_locked_user", user: locked_user.name)) end end diff --git a/spec/features/work_packages/work_package_index_spec.rb b/spec/features/work_packages/work_package_index_spec.rb index 14f9bd54160c..0bad6de1dcc7 100644 --- a/spec/features/work_packages/work_package_index_spec.rb +++ b/spec/features/work_packages/work_package_index_spec.rb @@ -29,13 +29,12 @@ require "spec_helper" RSpec.describe "Work Packages", "index view", :js, :with_cuprite do - let(:user) { create(:admin) } - let(:project) { create(:project, enabled_module_names: %w[work_package_tracking]) } + shared_let(:user) { create(:admin) } + shared_let(:project) { create(:project, enabled_module_names: %w[work_package_tracking]) } + let(:wp_table) { Pages::WorkPackagesTable.new(project) } - before do - login_as(user) - end + current_user { user } context "within a global context" do before do diff --git a/spec/features/wysiwyg/macros/attribute_macros_spec.rb b/spec/features/wysiwyg/macros/attribute_macros_spec.rb index c85aadc7a0b6..a1d8a7c6c9b3 100644 --- a/spec/features/wysiwyg/macros/attribute_macros_spec.rb +++ b/spec/features/wysiwyg/macros/attribute_macros_spec.rb @@ -31,34 +31,7 @@ RSpec.describe "Wysiwyg attribute macros", :js do shared_let(:admin) { create(:admin) } let(:user) { admin } - - shared_let(:type_milestone) { create(:type_milestone) } - shared_let(:type_task) { create(:type_task) } - - shared_let(:project) do - create(:project, - identifier: "some-project", - types: [type_milestone, type_task], - enabled_module_names: %w[wiki work_package_tracking]) - end - shared_let(:work_package) do - create(:work_package, - subject: "Foo Bar", - project:, - start_date: '2023-01-01', - due_date: '2023-01-05', - type: type_task) - end - shared_let(:milestone) do - create(:work_package, - subject: "Milestone", - project:, - due_date: '2023-01-10', - type: type_milestone) - end - let(:editor) { Components::WysiwygEditor.new } - let(:markdown) do <<~MD # My headline @@ -100,6 +73,31 @@ MD end + shared_let(:type_milestone) { create(:type_milestone) } + shared_let(:type_task) { create(:type_task) } + + shared_let(:project) do + create(:project, + identifier: "some-project", + types: [type_milestone, type_task], + enabled_module_names: %w[wiki work_package_tracking]) + end + shared_let(:work_package) do + create(:work_package, + subject: "Foo Bar", + project:, + start_date: "2023-01-01", + due_date: "2023-01-05", + type: type_task) + end + shared_let(:milestone) do + create(:work_package, + subject: "Milestone", + project:, + due_date: "2023-01-10", + type: type_milestone) + end + before do login_as(user) end @@ -186,4 +184,22 @@ end end end + + describe "recursively referencing descriptions (Regression #55320)" do + let(:wp_page) { Pages::FullWorkPackage.new(work_package) } + + before do + work_package.update_column(:description, "Hello from wp workPackageValue:##{milestone.id}:description") + milestone.update_column(:description, "Hello from milestone workPackageValue:##{work_package.id}:description") + end + + it "does not runaway" do + wp_page.visit! + + expect(page).to have_text("Hello from wp") + expect(page).to have_text("Hello from milestone") + + expect(page).to have_text("This macro is recursively referencing workPackage ##{milestone.id}") + end + end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 7b5636d71aef..c47cc8bcd961 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -40,9 +40,9 @@ Queries::Projects::Selects::Status.new(:project_status) ] - query_instance = instance_double(Queries::Projects::ProjectQuery, available_selects: selects) + query_instance = instance_double(ProjectQuery, available_selects: selects) - allow(Queries::Projects::ProjectQuery) + allow(ProjectQuery) .to receive(:new) .and_return(query_instance) end diff --git a/spec/httpx_spec.rb b/spec/httpx_spec.rb index 82208747e9ee..7bb608bb09ff 100644 --- a/spec/httpx_spec.rb +++ b/spec/httpx_spec.rb @@ -1,16 +1,16 @@ -require 'webrick' -require 'httpx' +require "webrick" +require "httpx" -RSpec.describe 'HTTPX' do - describe 'persistent connections' do - it 'does not hang forever when used to request HTTP 1.1 server' do - server = WEBrick::HTTPServer.new(:Port => 8543) - server.mount_proc '/' do |req, res| - res.body = 'Response Body' +RSpec.describe "HTTPX" do + describe "persistent connections" do + it "does not hang forever when used to request HTTP 1.1 server" do + server = WEBrick::HTTPServer.new(Port: 8543) + server.mount_proc "/" do |_req, res| + res.body = "Response Body" end Thread.new { server.start } - session = HTTPX .plugin(:persistent).with(timeout: {keep_alive_timeout: 2}) + session = HTTPX.plugin(:persistent).with(timeout: { keep_alive_timeout: 2 }) number_of_requests_made = 0 begin Timeout.timeout(10) do diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index 540315efb2c3..b921ccc1ec6d 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'i18n/tasks' +require "i18n/tasks" RSpec.describe I18n do let(:i18n) { I18n::Tasks::BaseTask.new } @@ -8,19 +8,19 @@ let(:unused_keys) { i18n.unused_keys } let(:inconsistent_interpolations) { i18n.inconsistent_interpolations } - it 'does not have missing keys' do - pending 'Enable when i18n-tasks is enabled across the project, otherwise this spec will report false positives' + it "does not have missing keys" do + pending "Enable when i18n-tasks is enabled across the project, otherwise this spec will report false positives" expect(missing_keys).to be_empty, "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them" end - it 'does not have unused keys' do + it "does not have unused keys" do expect(unused_keys).to be_empty, "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them" end - it 'files are normalized' do + it "files are normalized" do non_normalized = i18n.non_normalized_paths error_message = "The following files need to be normalized:\n" \ "#{non_normalized.map { |path| " #{path}" }.join("\n")}\n" \ @@ -28,12 +28,12 @@ expect(non_normalized).to be_empty, error_message end - context 'for all i18n files' do - let(:root_dir) { Pathname.new(__FILE__).dirname.join('..') } - let(:config_file) { root_dir.join('config/i18n-tasks-all-files.yml') } + context "for all i18n files" do + let(:root_dir) { Pathname.new(__FILE__).dirname.join("..") } + let(:config_file) { root_dir.join("config/i18n-tasks-all-files.yml") } let(:i18n) { I18n::Tasks::BaseTask.new(config_file:) } - it 'does not have inconsistent interpolations' do + it "does not have inconsistent interpolations" do config_file_relative_path = config_file.relative_path_from(Dir.pwd) error_message = "#{inconsistent_interpolations.leaves.count} i18n keys have inconsistent interpolations.\n" \ "Run 'i18n-tasks check-consistent-interpolations --config #{config_file_relative_path}' to show them" diff --git a/spec/lib/open_project/access_control/permission_spec.rb b/spec/lib/open_project/access_control/permission_spec.rb index de9eb7ea7b59..53d4496de330 100644 --- a/spec/lib/open_project/access_control/permission_spec.rb +++ b/spec/lib/open_project/access_control/permission_spec.rb @@ -79,6 +79,68 @@ end end + describe "#permissible_on?" do + context "when marked as permissible on work package roles" do + subject(:permission) do + described_class.new(:perm, { cont: [:action] }, permissible_on: :work_package) + end + + it { expect(permission).to be_permissible_on(WorkPackage.new) } + it { expect(permission).not_to be_permissible_on(Project.new) } + it { expect(permission).not_to be_permissible_on(nil) } + it { expect(permission).not_to be_permissible_on(ProjectQuery.new) } + it { expect(permission).to be_permissible_on(:work_package) } + it { expect(permission).not_to be_permissible_on(:project) } + it { expect(permission).not_to be_permissible_on(:global) } + it { expect(permission).not_to be_permissible_on(:project_query) } + end + + context "when marked as permissible on project roles" do + subject(:permission) do + described_class.new(:perm, { cont: [:action] }, permissible_on: :project) + end + + it { expect(permission).not_to be_permissible_on(WorkPackage.new) } + it { expect(permission).to be_permissible_on(Project.new) } + it { expect(permission).not_to be_permissible_on(nil) } + it { expect(permission).not_to be_permissible_on(ProjectQuery.new) } + it { expect(permission).not_to be_permissible_on(:work_package) } + it { expect(permission).to be_permissible_on(:project) } + it { expect(permission).not_to be_permissible_on(:global) } + it { expect(permission).not_to be_permissible_on(:project_query) } + end + + context "when marked as permissible on global roles" do + subject(:permission) do + described_class.new(:perm, { cont: [:action] }, permissible_on: :global) + end + + it { expect(permission).not_to be_permissible_on(WorkPackage.new) } + it { expect(permission).not_to be_permissible_on(Project.new) } + it { expect(permission).to be_permissible_on(nil) } + it { expect(permission).not_to be_permissible_on(ProjectQuery.new) } + it { expect(permission).not_to be_permissible_on(:work_package) } + it { expect(permission).not_to be_permissible_on(:project) } + it { expect(permission).to be_permissible_on(:global) } + it { expect(permission).not_to be_permissible_on(:project_query) } + end + + context "when marked as permissible on project queries" do + subject(:permission) do + described_class.new(:perm, { cont: [:action] }, permissible_on: :project_query) + end + + it { expect(permission).not_to be_permissible_on(WorkPackage.new) } + it { expect(permission).not_to be_permissible_on(Project.new) } + it { expect(permission).not_to be_permissible_on(nil) } + it { expect(permission).to be_permissible_on(ProjectQuery.new) } + it { expect(permission).not_to be_permissible_on(:work_package) } + it { expect(permission).not_to be_permissible_on(:project) } + it { expect(permission).not_to be_permissible_on(:global) } + it { expect(permission).to be_permissible_on(:project_query) } + end + end + describe "marking it as permissible on multiple role types" do subject(:permission) do described_class.new(:perm, { cont: [:action] }, permissible_on: %i[work_package project]) diff --git a/spec/lib/open_project/events_spec.rb b/spec/lib/open_project/events_spec.rb index da7d20b7251b..3ee1fcb8ec9e 100644 --- a/spec/lib/open_project/events_spec.rb +++ b/spec/lib/open_project/events_spec.rb @@ -38,6 +38,7 @@ def fire_event(event_constant_name) before do allow(Storages::ManageStorageIntegrationsJob).to receive(:debounce) + allow(Storages::AutomaticallyManagedStorageSyncJob).to receive(:debounce) end %w[ @@ -53,16 +54,16 @@ def fire_event(event_constant_name) it do subject - expect(Storages::ManageStorageIntegrationsJob).not_to have_received(:debounce) + expect(Storages::AutomaticallyManagedStorageSyncJob).not_to have_received(:debounce) end end context "when payload contains automatic project_folder_mode" do - let(:payload) { { project_folder_mode: :automatic } } + let(:payload) { { project_folder_mode: :automatic, storage: create(:nextcloud_storage) } } it do subject - expect(Storages::ManageStorageIntegrationsJob).to have_received(:debounce) + expect(Storages::AutomaticallyManagedStorageSyncJob).to have_received(:debounce).with(payload[:storage]) end it do @@ -78,19 +79,36 @@ def fire_event(event_constant_name) MEMBER_CREATED MEMBER_UPDATED MEMBER_DESTROYED - PROJECT_UPDATED - PROJECT_RENAMED - PROJECT_ARCHIVED - PROJECT_UNARCHIVED ].each do |event| describe(event) do + let(:project_role) { create(:existing_project_role) } + let(:project_storage) { create(:project_storage) } + let(:member) { create(:work_package_member, roles: [project_role], project: project_storage.project) } + + let(:payload) { { member: } } + subject { fire_event(event) } - let(:payload) { {} } + it do + subject + expect(Storages::AutomaticallyManagedStorageSyncJob).to have_received(:debounce).with(project_storage.storage) + end + end + end + + %w[PROJECT_UPDATED + PROJECT_RENAMED + PROJECT_ARCHIVED + PROJECT_UNARCHIVED].each do |event| + describe(event) do + let(:project_storage) { create(:project_storage, :as_automatically_managed) } + let(:payload) { { project: project_storage.project } } + + subject { fire_event(event) } it do subject - expect(Storages::ManageStorageIntegrationsJob).to have_received(:debounce) + expect(Storages::AutomaticallyManagedStorageSyncJob).to have_received(:debounce).with(project_storage.storage) end end end diff --git a/spec/helpers/menus/projects_spec.rb b/spec/menus/projects/menu_spec.rb similarity index 90% rename from spec/helpers/menus/projects_spec.rb rename to spec/menus/projects/menu_spec.rb index 8dfa6436e98b..461ef99de5bf 100644 --- a/spec/helpers/menus/projects_spec.rb +++ b/spec/menus/projects/menu_spec.rb @@ -30,7 +30,7 @@ require "spec_helper" -RSpec.describe Menus::Projects do +RSpec.describe Projects::Menu do let(:instance) { described_class.new(controller_path:, params:, current_user:) } let(:controller_path) { "foo" } let(:params) { {} } @@ -38,26 +38,26 @@ shared_let(:current_user) { build(:user) } shared_let(:current_user_query) do - Queries::Projects::ProjectQuery.create!(name: "Current user query", user: current_user) + ProjectQuery.create!(name: "Current user query", user: current_user) end shared_let(:other_user_query) do - Queries::Projects::ProjectQuery.create!(name: "Other user query", user: build(:user)) + ProjectQuery.create!(name: "Other user query", user: build(:user)) end shared_let(:public_query) do - Queries::Projects::ProjectQuery.create!(name: "Public query", user: build(:user), public: true) + ProjectQuery.create!(name: "Public query", user: build(:user), public: true) end - subject(:first_level_menu_items) { instance.first_level_menu_items } + subject(:menu_items) { instance.menu_items } it "returns 4 menu groups" do - expect(first_level_menu_items).to all(be_a(OpenProject::Menu::MenuGroup)) - expect(first_level_menu_items.length).to eq(4) + expect(menu_items).to all(be_a(OpenProject::Menu::MenuGroup)) + expect(menu_items.length).to eq(4) end describe "children items" do - subject(:children_menu_items) { first_level_menu_items.flat_map(&:children) } + subject(:children_menu_items) { menu_items.flat_map(&:children) } context "when the current user is an admin" do before do @@ -97,7 +97,7 @@ end describe "selected children items" do - subject(:selected_menu_items) { first_level_menu_items.flat_map(&:children).select(&:selected) } + subject(:selected_menu_items) { menu_items.flat_map(&:children).select(&:selected) } context "when on homescreen page" do let(:controller_path) { "homescreen" } diff --git a/spec/models/queries/projects/project_queries/scopes/allowed_to_spec.rb b/spec/models/project_queries/scopes/allowed_to_spec.rb similarity index 96% rename from spec/models/queries/projects/project_queries/scopes/allowed_to_spec.rb rename to spec/models/project_queries/scopes/allowed_to_spec.rb index bb6eb80ab6e8..95f51a79c218 100644 --- a/spec/models/queries/projects/project_queries/scopes/allowed_to_spec.rb +++ b/spec/models/project_queries/scopes/allowed_to_spec.rb @@ -28,7 +28,7 @@ require "spec_helper" -RSpec.describe Queries::Projects::ProjectQuery, "#allowed to" do # rubocop:disable RSpec/FilePath,RSpec/SpecFilePathFormat +RSpec.describe ProjectQuery, "#allowed to" do # rubocop:disable RSpec/RSpec/SpecFilePathFormat shared_let(:user) { create(:user) } shared_let(:other_user) { create(:user) } diff --git a/spec/models/queries/projects/factory_spec.rb b/spec/models/queries/projects/factory_spec.rb index c4fc2fc8fa0e..4399c1d05cd8 100644 --- a/spec/models/queries/projects/factory_spec.rb +++ b/spec/models/queries/projects/factory_spec.rb @@ -34,7 +34,7 @@ before do scope = instance_double(ActiveRecord::Relation) - allow(Queries::Projects::ProjectQuery) + allow(ProjectQuery) .to receive(:visible) .with(current_user) .and_return(scope) @@ -87,7 +87,7 @@ context "without id" do it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has a name" do @@ -135,7 +135,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has a name" do @@ -166,7 +166,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has a name" do @@ -197,7 +197,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has a name" do @@ -228,7 +228,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has a name" do @@ -259,7 +259,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has a name" do @@ -290,7 +290,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has a name" do @@ -389,7 +389,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has no name" do @@ -421,7 +421,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has no name" do @@ -468,7 +468,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has no name" do @@ -515,7 +515,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has no name" do @@ -551,7 +551,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has no name" do @@ -583,7 +583,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has no name" do @@ -632,7 +632,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "keeps the name" do @@ -677,7 +677,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "keeps the name" do @@ -713,7 +713,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "keeps the name" do @@ -781,7 +781,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has no name" do @@ -838,7 +838,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "keeps the name" do @@ -888,7 +888,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has a name" do @@ -917,7 +917,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has a name" do @@ -946,7 +946,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has a name" do @@ -975,7 +975,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has a name" do @@ -1004,7 +1004,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has a name" do @@ -1033,7 +1033,7 @@ it "returns a project query" do expect(find) - .to be_a(Queries::Projects::ProjectQuery) + .to be_a(ProjectQuery) end it "has a name" do diff --git a/spec/models/queries/projects/filters/custom_field_filter_spec.rb b/spec/models/queries/projects/filters/custom_field_filter_spec.rb index 2bae736f5a42..1b576ab79990 100644 --- a/spec/models/queries/projects/filters/custom_field_filter_spec.rb +++ b/spec/models/queries/projects/filters/custom_field_filter_spec.rb @@ -29,7 +29,7 @@ require "spec_helper" RSpec.describe Queries::Projects::Filters::CustomFieldFilter do - let(:query) { Queries::Projects::ProjectQuery.new } + let(:query) { ProjectQuery.new } let(:bool_project_custom_field) { build_stubbed(:boolean_project_custom_field) } let(:int_project_custom_field) { build_stubbed(:integer_project_custom_field) } let(:float_project_custom_field) { build_stubbed(:float_project_custom_field) } diff --git a/spec/models/queries/projects/project_query_results_spec.rb b/spec/models/queries/projects/project_query_results_spec.rb index ecbb349d9258..acfb44fc36f1 100644 --- a/spec/models/queries/projects/project_query_results_spec.rb +++ b/spec/models/queries/projects/project_query_results_spec.rb @@ -28,7 +28,7 @@ require "spec_helper" -RSpec.describe Queries::Projects::ProjectQuery, "results" do +RSpec.describe ProjectQuery, "results" do let(:instance) { described_class.new } let(:base_scope) { Project.order(id: :desc) } diff --git a/spec/models/queries/projects/project_query_spec.rb b/spec/models/queries/projects/project_query_spec.rb index cb19bf27363a..841f992b9b75 100644 --- a/spec/models/queries/projects/project_query_spec.rb +++ b/spec/models/queries/projects/project_query_spec.rb @@ -28,7 +28,7 @@ require "spec_helper" -RSpec.describe Queries::Projects::ProjectQuery do +RSpec.describe ProjectQuery do let(:instance) { described_class.new } shared_let(:user) { create(:user) } diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb index 21f2ccc013f9..585c4b31f8c8 100644 --- a/spec/models/setting_spec.rb +++ b/spec/models/setting_spec.rb @@ -180,6 +180,23 @@ .to raise_error NoMethodError end + context "for a setting with an environment specific default value", :settings_reset do + before do + Settings::Definition.add( + "my_setting", + format: :string, + default: "The default", + default_by_env: { + test: "The test default" + } + ) + end + + it "uses the test specific default" do + expect(described_class.my_setting).to eq("The test default") + end + end + context "for a integer setting with non-nil default value", :settings_reset do before do Settings::Definition.add( diff --git a/spec/models/users/permission_checks_spec.rb b/spec/models/users/permission_checks_spec.rb index 765b04560cd9..3a5a9082548f 100644 --- a/spec/models/users/permission_checks_spec.rb +++ b/spec/models/users/permission_checks_spec.rb @@ -179,31 +179,55 @@ let(:public_permissions) { OpenProject::AccessControl.public_permissions.map(&:name) } - subject do - create(:user, global_permissions: [:create_user], - member_with_permissions: { project => %i[view_work_packages edit_work_packages] }) - end + context "for a non admin user" do + subject do + create(:user, global_permissions: [:create_user], + member_with_permissions: { project => %i[view_work_packages edit_work_packages] }) + end - it "returns all permissions given on the project" do - expect(subject.all_permissions_for(project)).to match_array(%i[view_work_packages edit_work_packages] + public_permissions) - end + it "returns all permissions given on the project" do + expect(subject.all_permissions_for(project)) + .to match_array(%i[view_work_packages edit_work_packages] + public_permissions) + end - it "returns non-member permissions given on the project the user is not a member of" do - expect(subject.all_permissions_for(other_project)).to match_array(%i[view_work_packages - manage_members] + public_permissions) - end + it "returns non-member permissions given on the project the user is not a member of" do + expect(subject.all_permissions_for(other_project)).to match_array(%i[view_work_packages + manage_members] + public_permissions) + end - it "returns all global permissions" do - skip "Current implementation of the Authorization.roles query returns ALL permissions the user has, not only global ones. " \ - "We should change this in the fututre, thats why this test is already in here." + it "returns all global permissions" do + skip "Current implementation of the Authorization.roles query returns ALL permissions the user has, " \ + "not only global ones. We should change this in the future, that's why this test is already in here." + + expect(subject.all_permissions_for(nil)).to match_array(%i[create_user]) + end - expect(subject.all_permissions_for(nil)).to match_array(%i[create_user]) + it "returns all permissions the user has (with project and global permissions)" do + expect(subject.all_permissions_for(nil)).to match_array(%i[create_user + view_work_packages edit_work_packages + manage_members] + public_permissions) + end end - it "returns all permissions the user has (with project and global permissions)" do - expect(subject.all_permissions_for(nil)).to match_array(%i[create_user - view_work_packages edit_work_packages - manage_members] + public_permissions) + context "for an admin user" do + subject do + create(:admin) + end + + it "returns all permissions given on the project" do + expect(subject.all_permissions_for(project)) + .to include(:view_work_packages, :edit_work_packages, *public_permissions) + end + + it "returns all permissions given globally" do + expect(subject.all_permissions_for(nil)) + .to include(:create_user) + end + + it "returns all permissions given on a work package" do + expect(subject.all_permissions_for(WorkPackage.new)) + .to include(:view_work_packages, :edit_work_packages) + end end end diff --git a/spec/models/work_package/work_package_acts_as_journalized_spec.rb b/spec/models/work_package/work_package_acts_as_journalized_spec.rb index 58d2229e2184..baf63383e108 100644 --- a/spec/models/work_package/work_package_acts_as_journalized_spec.rb +++ b/spec/models/work_package/work_package_acts_as_journalized_spec.rb @@ -26,10 +26,10 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require 'spec_helper' +require "spec_helper" RSpec.describe WorkPackage do - describe '#journal' do + describe "#journal" do let(:type) { create(:type) } let(:project) do create(:project, @@ -42,7 +42,7 @@ create(:work_package, project_id: project.id, type:, - description: 'Description', + description: "Description", priority:, status:, duration: 1) @@ -52,53 +52,53 @@ current_user { create(:user) } - context 'for work package creation' do + context "for work package creation" do it { expect(Journal.for_work_package.count).to eq(1) } - it 'has a journal entry' do + it "has a journal entry" do expect(Journal.for_work_package.first.journable).to eq(work_package) end - it 'notes the changes to subject' do + it "notes the changes to subject" do expect(work_package.last_journal.details[:subject]) .to contain_exactly(nil, work_package.subject) end - it 'notes the changes to project' do + it "notes the changes to project" do expect(work_package.last_journal.details[:project_id]) .to contain_exactly(nil, work_package.project_id) end - it 'notes the description' do + it "notes the description" do expect(work_package.last_journal.details[:description]) .to contain_exactly(nil, work_package.description) end - it 'notes the scheduling mode' do + it "notes the scheduling mode" do expect(work_package.last_journal.details[:schedule_manually]) .to contain_exactly(nil, false) end - it 'has the timestamp of the work package update time for created_at' do + it "has the timestamp of the work package update time for created_at" do expect(work_package.last_journal.created_at) .to eql(work_package.reload.updated_at) end - it 'has the updated_at of the work package as the lower bound for validity_period and no upper bound' do + it "has the updated_at of the work package as the lower bound for validity_period and no upper bound" do expect(work_package.last_journal.validity_period) .to eql(work_package.reload.updated_at...) end end - context 'when nothing is changed' do + context "when nothing is changed" do it { expect { work_package.save! }.not_to change(Journal, :count) } - it 'does not update the updated_at time of the work package' do + it "does not update the updated_at time of the work package" do expect { work_package.save! }.not_to change(work_package, :updated_at) end end - context 'for different newlines', with_settings: { journal_aggregation_time_minutes: 0 } do + context "for different newlines", with_settings: { journal_aggregation_time_minutes: 0 } do let(:description) { "Description\n\nwith newlines\n\nembedded" } let(:changed_description) { description.gsub("\n", "\r\n") } let!(:work_package1) do @@ -113,19 +113,19 @@ work_package1.description = changed_description end - context 'when a new journal is created tracking a simultaneously applied change' do + context "when a new journal is created tracking a simultaneously applied change" do before do - work_package1.subject += 'changed' + work_package1.subject += "changed" work_package1.save! end - describe 'does not track the changed newline characters' do + describe "does not track the changed newline characters" do subject { work_package1.last_journal.data.description } it { is_expected.to eq(description) } end - describe 'tracks only the other change' do + describe "tracks only the other change" do subject { work_package1.last_journal.details } it { is_expected.to have_key :subject } @@ -133,7 +133,7 @@ end end - context 'when there is a legacy journal containing non-escaped newlines' do + context "when there is a legacy journal containing non-escaped newlines" do let!(:work_package1) do create(:work_package, journals: { @@ -142,13 +142,13 @@ }) end - it 'does not track the change to the newline characters' do + it "does not track the change to the newline characters" do expect(work_package1.reload.last_journal.details).not_to have_key :description end end end - describe 'on work package change without aggregation', with_settings: { journal_aggregation_time_minutes: 0 } do + describe "on work package change without aggregation", with_settings: { journal_aggregation_time_minutes: 0 } do let(:parent_work_package) do create(:work_package, project_id: project.id, @@ -162,8 +162,8 @@ before do project.types << type2 - work_package.subject = 'changed' - work_package.description = 'changed' + work_package.subject = "changed" + work_package.description = "changed" work_package.type = type2 work_package.status = status2 work_package.priority = priority2 @@ -179,10 +179,10 @@ work_package.save! end - context 'for last created journal' do + context "for last created journal" do subject { work_package.last_journal.details } - it 'contains all changes' do + it "contains all changes" do %i(subject description type_id status_id priority_id start_date due_date estimated_hours assigned_to_id responsible_id parent_id schedule_manually duration).each do |a| @@ -191,106 +191,106 @@ end end - shared_examples_for 'old value' do + shared_examples_for "old value" do subject { work_package.last_journal.old_value_for(property) } it { is_expected.to eq(expected_value) } end - shared_examples_for 'new value' do + shared_examples_for "new value" do subject { work_package.last_journal.new_value_for(property) } it { is_expected.to eq(expected_value) } end - describe 'journaled value for' do - describe 'description' do - let(:property) { 'description' } + describe "journaled value for" do + describe "description" do + let(:property) { "description" } - context 'for old value' do - let(:expected_value) { 'Description' } + context "for old value" do + let(:expected_value) { "Description" } - it_behaves_like 'old value' + it_behaves_like "old value" end - context 'for new value' do - let(:expected_value) { 'changed' } + context "for new value" do + let(:expected_value) { "changed" } - it_behaves_like 'new value' + it_behaves_like "new value" end end - describe 'schedule_manually' do - let(:property) { 'schedule_manually' } + describe "schedule_manually" do + let(:property) { "schedule_manually" } - context 'for old value' do + context "for old value" do let(:expected_value) { false } - it_behaves_like 'old value' + it_behaves_like "old value" end - context 'for new value' do + context "for new value" do let(:expected_value) { true } - it_behaves_like 'new value' + it_behaves_like "new value" end end - describe 'duration' do - let(:property) { 'duration' } + describe "duration" do + let(:property) { "duration" } - context 'for old value' do + context "for old value" do let(:expected_value) { 1 } - it_behaves_like 'old value' + it_behaves_like "old value" end - context 'for new value' do + context "for new value" do let(:expected_value) { 8 } - it_behaves_like 'new value' + it_behaves_like "new value" end end end - describe 'adding journal with a missing journal and an existing journal' do + describe "adding journal with a missing journal and an existing journal" do before do allow(WorkPackages::UpdateContract).to receive(:new).and_return(NoopContract.new) service = WorkPackages::UpdateService.new(user: current_user, model: work_package) - service.call(journal_notes: 'note to be deleted', send_notifications: false) + service.call(journal_notes: "note to be deleted", send_notifications: false) work_package.reload - service.call(description: 'description v2', send_notifications: false) + service.call(description: "description v2", send_notifications: false) work_package.reload - work_package.journals.reload.find_by(notes: 'note to be deleted').delete + work_package.journals.reload.find_by(notes: "note to be deleted").delete - service.call(description: 'description v4', send_notifications: false) + service.call(description: "description v4", send_notifications: false) end - it 'creates a journal for the last change' do + it "creates a journal for the last change" do last_journal = work_package.last_journal - expect(last_journal.data.description).to eql('description v4') + expect(last_journal.data.description).to eql("description v4") end end - it 'has the timestamp of the work package update time for created_at' do + it "has the timestamp of the work package update time for created_at" do expect(work_package.last_journal.created_at) .to eql(work_package.reload.updated_at) end - it 'has the updated_at of the work package as the lower bound for validity_period and no upper bound' do + it "has the updated_at of the work package as the lower bound for validity_period and no upper bound" do expect(work_package.last_journal.validity_period) .to eql(work_package.reload.updated_at...) end - it 'sets the upper bound of the preceeding journal to be the created_at time of the newly created journal' do + it "sets the upper bound of the preceeding journal to be the created_at time of the newly created journal" do former_last_journal = work_package.journals[-2] expect(former_last_journal.validity_period) .to eql(former_last_journal.created_at...work_package.last_journal.created_at) end end - describe 'attachments', with_settings: { journal_aggregation_time_minutes: 0 } do + describe "attachments", with_settings: { journal_aggregation_time_minutes: 0 } do let(:attachment) { build(:attachment) } let(:attachment_id) { "attachments_#{attachment.id}" } @@ -299,7 +299,7 @@ work_package.save! end - context 'for new attachment' do + context "for new attachment" do subject { work_package.last_journal.details } it { is_expected.to have_key attachment_id } @@ -307,22 +307,22 @@ it { expect(subject[attachment_id]).to eq([nil, attachment.filename]) } end - context 'when attachment saved w/o change' do + context "when attachment saved w/o change" do it { expect { attachment.save! }.not_to change(Journal, :count) } end end - describe 'custom values', with_settings: { journal_aggregation_time_minutes: 0 } do + describe "custom values", with_settings: { journal_aggregation_time_minutes: 0 } do let(:custom_field) { create(:work_package_custom_field) } let(:custom_value) do build(:custom_value, - value: 'false', + value: "false", custom_field:) end let(:custom_field_id) { "custom_fields_#{custom_value.custom_field_id}" } - shared_context 'for work package with custom value' do + shared_context "for work package with custom value" do before do project.work_package_custom_fields << custom_field type.custom_fields << custom_field @@ -332,8 +332,8 @@ end end - context 'for new custom value' do - include_context 'for work package with custom value' + context "for new custom value" do + include_context "for work package with custom value" subject { work_package.last_journal.details } @@ -342,12 +342,12 @@ it { expect(subject[custom_field_id]).to eq([nil, custom_value.value]) } end - context 'for custom value modified' do - include_context 'for work package with custom value' + context "for custom value modified" do + include_context "for work package with custom value" let(:modified_custom_value) do create(:work_package_custom_value, - value: 'true', + value: "true", custom_field:) end @@ -363,12 +363,12 @@ it { expect(subject[custom_field_id]).to eq([custom_value.value.to_s, modified_custom_value.value.to_s]) } end - context 'when work package saved w/o change' do - include_context 'for work package with custom value' + context "when work package saved w/o change" do + include_context "for work package with custom value" let(:unmodified_custom_value) do create(:work_package_custom_value, - value: 'false', + value: "false", custom_field:) end @@ -378,15 +378,15 @@ it { expect { work_package.save! }.not_to change(Journal, :count) } - it 'does not set an upper bound to the already existing journal' do + it "does not set an upper bound to the already existing journal" do work_package.save expect(work_package.last_journal.validity_period.end) .to be_nil end end - context 'when custom value removed' do - include_context 'for work package with custom value' + context "when custom value removed" do + include_context "for work package with custom value" before do work_package.custom_values.delete(custom_value) @@ -400,35 +400,35 @@ it { expect(subject[custom_field_id]).to eq([custom_value.value, nil]) } end - context 'when custom value did not exist before' do + context "when custom value did not exist before" do let(:custom_field) do create(:work_package_custom_field, is_required: false, - field_format: 'list', - possible_values: ['', '1', '2', '3', '4', '5', '6', '7']) + field_format: "list", + possible_values: ["", "1", "2", "3", "4", "5", "6", "7"]) end let(:custom_value) do create(:custom_value, - value: '', + value: "", customized: work_package, custom_field:) end - describe 'empty values are recognized as unchanged' do - include_context 'for work package with custom value' + describe "empty values are recognized as unchanged" do + include_context "for work package with custom value" it { expect(work_package.last_journal.customizable_journals).to be_empty } end - describe 'empty values handled as non existing' do - include_context 'for work package with custom value' + describe "empty values handled as non existing" do + include_context "for work package with custom value" it { expect(work_package.last_journal.customizable_journals.count).to eq(0) } end end end - describe 'file_links', with_settings: { journal_aggregation_time_minutes: 0 } do + describe "file_links", with_settings: { journal_aggregation_time_minutes: 0 } do let(:file_link) { build(:file_link) } let(:file_link_id) { "file_links_#{file_link.id}" } @@ -437,18 +437,18 @@ work_package.save! end - context 'for the new file link' do + context "for the new file link" do subject(:journal_details) { work_package.last_journal.details } it { is_expected.to have_key file_link_id } it { expect(journal_details[file_link_id]) - .to eq([nil, { 'link_name' => file_link.origin_name, 'storage_name' => nil }]) + .to eq([nil, { "link_name" => file_link.origin_name, "storage_name" => nil }]) } end - context 'when file link saved w/o change' do + context "when file link saved w/o change" do it { expect do file_link.save @@ -458,56 +458,56 @@ end end - context 'for only journal notes adding' do + context "for only journal notes adding" do subject do - work_package.add_journal(user: User.current, notes: 'some notes') + work_package.add_journal(user: User.current, notes: "some notes") work_package.save work_package end - it 'does not create a new journal entry' do + it "does not create a new journal entry" do expect { subject }.not_to change(work_package, :last_journal) end - it 'has the timestamp of the work package update time for updated_at' do + it "has the timestamp of the work package update time for updated_at" do expect(subject.last_journal.updated_at).to eql(work_package.reload.updated_at) end - it 'updates the updated_at time of the work package' do + it "updates the updated_at time of the work package" do expect { subject.reload }.to change(work_package, :updated_at) end - it 'stores the note with the existing journal entry' do - expect { subject }.to change { work_package.last_journal.notes }.from('').to('some notes') + it "stores the note with the existing journal entry" do + expect { subject }.to change { work_package.last_journal.notes }.from("").to("some notes") end end - context 'for mixed journal notes and attribute adding' do + context "for mixed journal notes and attribute adding" do subject do - work_package.add_journal(user: User.current, notes: 'some notes') - work_package.subject = 'blubs' + work_package.add_journal(user: User.current, notes: "some notes") + work_package.subject = "blubs" work_package.save work_package end - it 'does not create a new journal entry' do + it "does not create a new journal entry" do expect { subject }.not_to change(work_package, :last_journal) end - it 'has the timestamp of the work package update time for updated_at' do + it "has the timestamp of the work package update time for updated_at" do expect(subject.last_journal.updated_at).to eql(work_package.reload.updated_at) end - it 'updates the updated_at time of the work package' do + it "updates the updated_at time of the work package" do expect { subject.reload }.to change(work_package, :updated_at) end - it 'stores the note with the existing journal entry' do - expect { subject }.to change { work_package.last_journal.notes }.from('').to('some notes') + it "stores the note with the existing journal entry" do + expect { subject }.to change { work_package.last_journal.notes }.from("").to("some notes") end end - context 'for only journal cause adding' do + context "for only journal cause adding" do subject do work_package.add_journal( user: User.current, @@ -517,62 +517,62 @@ work_package end - it 'has the cause logged in the last journal' do + it "has the cause logged in the last journal" do expect(subject.last_journal.cause).to eql({ - 'type' => 'work_package_predecessor_changed_times', - 'work_package_id' => other_work_package.id + "type" => "work_package_predecessor_changed_times", + "work_package_id" => other_work_package.id }) end - it 'has the timestamp of the work package update time for created_at' do + it "has the timestamp of the work package update time for created_at" do expect(subject.last_journal.updated_at).to eql(work_package.reload.updated_at) end - it 'updates the updated_at time of the work package' do + it "updates the updated_at time of the work package" do expect { subject.reload }.to change(work_package, :updated_at) end - it 'does create a new journal entry' do + it "does create a new journal entry" do expect { subject }.to change(work_package, :last_journal) end end - context 'for mixed journal cause, notes and attribute adding' do + context "for mixed journal cause, notes and attribute adding" do subject do work_package.add_journal( user: User.current, - notes: 'some notes', + notes: "some notes", cause: Journal::CausedByWorkPackagePredecessorChange.new(other_work_package) ) - work_package.subject = 'blubs' + work_package.subject = "blubs" work_package.save work_package end - it 'has the timestamp of the work package update time for created_at' do + it "has the timestamp of the work package update time for created_at" do expect(work_package.last_journal.updated_at).to eql(work_package.reload.updated_at) end - it 'does create a new journal entry' do + it "does create a new journal entry" do expect { subject }.to change(work_package, :last_journal) end - it 'updates the updated_at time of the work package' do + it "updates the updated_at time of the work package" do updated_at_before = work_package.updated_at expect(subject.reload.updated_at).not_to eql(updated_at_before) end - it 'stores the cause and note with the existing journal entry' do + it "stores the cause and note with the existing journal entry" do subject - expect(work_package.last_journal.notes).to eq('some notes') - expect(work_package.last_journal.cause_type).to eq('work_package_predecessor_changed_times') + expect(work_package.last_journal.notes).to eq("some notes") + expect(work_package.last_journal.cause_type).to eq("work_package_predecessor_changed_times") expect(work_package.last_journal.cause_work_package_id).to eq(other_work_package.id) end end - context 'when 2 updates with the same cause occur' do + context "when 2 updates with the same cause occur" do before do work_package.add_journal( user: User.current, @@ -591,20 +591,20 @@ work_package.save end - it 'does not create a new journal entry' do + it "does not create a new journal entry" do expect { subject }.not_to change(work_package, :last_journal) end - it 'stores the last update only' do + it "stores the last update only" do subject - expect(work_package.last_journal.new_value_for(:subject)).to eq('new subject 2') - expect(work_package.last_journal.cause_type).to eq('work_package_predecessor_changed_times') + expect(work_package.last_journal.new_value_for(:subject)).to eq("new subject 2") + expect(work_package.last_journal.cause_type).to eq("work_package_predecessor_changed_times") expect(work_package.last_journal.cause_work_package_id).to eq(other_work_package.id) end end - context 'when updated within aggregation time' do + context "when updated within aggregation time" do subject(:journals) { work_package.journals } current_user { user1 } @@ -627,18 +627,18 @@ work_package.save! end - context 'as author of last change' do + context "as author of last change" do let(:new_author) { user1 } - it 'leads to a single journal' do + it "leads to a single journal" do expect(subject.count).to be 1 end - it 'is the initial journal' do + it "is the initial journal" do expect(subject.first).to be_initial end - it 'contains the changes of both updates with the later overwriting the former' do + it "contains the changes of both updates with the later overwriting the former" do expect(subject.first.data.status_id) .to eql changes[:status].id @@ -646,51 +646,51 @@ .to eql work_package.type_id end - context 'with a comment' do - let(:notes) { 'This is why I changed it.' } + context "with a comment" do + let(:notes) { "This is why I changed it." } - it 'leads to a single journal with the comment' do + it "leads to a single journal with the comment" do expect(subject.count).to be 1 expect(subject.first.notes) .to eql notes end - context 'when adding a second comment' do - let(:second_notes) { 'Another comment, unrelated to the first one.' } + context "when adding a second comment" do + let(:second_notes) { "Another comment, unrelated to the first one." } before do work_package.add_journal(user: new_author, notes: second_notes) work_package.save! end - it 'returns two journals' do + it "returns two journals" do expect(subject.count).to be 2 expect(subject.first.notes).to eql notes expect(subject.second.notes).to eql second_notes end - it 'has one initial journal and one non-initial journal' do + it "has one initial journal and one non-initial journal" do expect(subject.first).to be_initial expect(subject.second).not_to be_initial end end - context 'when adding another change without comment' do + context "when adding another change without comment" do before do work_package.reload # need to update the lock_version, avoiding StaleObjectError - work_package.subject = 'foo' + work_package.subject = "foo" work_package.assigned_to = current_user work_package.save! end - it 'leads to a single journal with the comment of the replaced journal and the state both combined' do + it "leads to a single journal with the comment of the replaced journal and the state both combined" do expect(subject.count).to eq 1 expect(subject.first.notes) .to eql notes expect(subject.first.data.subject) - .to eql 'foo' + .to eql "foo" expect(subject.first.data.assigned_to) .to eql current_user @@ -700,16 +700,16 @@ end end - context 'when adding another change with a customized work package' do + context "when adding another change with a customized work package" do let(:custom_field) do create(:work_package_custom_field, is_required: false, - field_format: 'list', - possible_values: ['', '1', '2', '3', '4', '5', '6', '7']) + field_format: "list", + possible_values: ["", "1", "2", "3", "4", "5", "6", "7"]) end let(:custom_value) do create(:custom_value, - value: custom_field.custom_options.find { |co| co.value == '1' }.try(:id), + value: custom_field.custom_options.find { |co| co.value == "1" }.try(:id), customized: work_package, custom_field:) end @@ -717,66 +717,66 @@ before do custom_value work_package.reload # need to update the lock_version, avoiding StaleObjectError - work_package.subject = 'foo' + work_package.subject = "foo" work_package.save! end - it 'leads to a single journal with only one customizable journal' do + it "leads to a single journal with only one customizable journal" do expect(subject.count).to eq 1 expect(subject.first.notes) .to eql notes expect(subject.first.data.subject) - .to eql 'foo' + .to eql "foo" expect(subject.first.customizable_journals.count).to eq(1) end end end - it 'has the journal\'s creation time as the lower and no upper bound for validity_period' do + it "has the journal's creation time as the lower and no upper bound for validity_period" do expect(work_package.last_journal.validity_period) .to eql(work_package.last_journal.created_at...) end end - context 'with a different author' do + context "with a different author" do let(:new_author) { user2 } - it 'leads to two journals' do + it "leads to two journals" do expect(subject.count).to be 2 end - it 'has the initial user as the author of the first journal' do + it "has the initial user as the author of the first journal" do expect(subject.first.user) .to eql current_user end - it 'has the second user as he author of the second journal' do + it "has the second user as he author of the second journal" do expect(subject.second.user) .to eql new_author end - it 'has the changes (compared to the initial state) in the second journal' do + it "has the changes (compared to the initial state) in the second journal" do expect(subject.second.get_changes) .to eql("status_id" => [status.id, new_status.id]) end - it 'has the first journal\'s creation time as the lower and the second journal\'s creation time ' \ - 'as the upper bound for validity_period of the first journal' do + it "has the first journal's creation time as the lower and the second journal's creation time " \ + "as the upper bound for validity_period of the first journal" do expect(subject.first.validity_period) .to eql(subject.first.created_at...subject.second.created_at) end - it 'has the second journal\'s creation time as the lower and no upper bound for validity_period of the second journal' do + it "has the second journal's creation time as the lower and no upper bound for validity_period of the second journal" do expect(subject.second.validity_period) .to eql(subject.second.created_at...) end end end - context 'when updated after aggregation timeout expired', with_settings: { journal_aggregation_time_minutes: 1 } do + context "when updated after aggregation timeout expired", with_settings: { journal_aggregation_time_minutes: 1 } do let(:last_update_time) { 2.minutes.ago } subject(:journals) { work_package.journals } @@ -792,38 +792,38 @@ work_package.save! end - it 'creates a new journal' do + it "creates a new journal" do expect(journals.count).to be 2 end - it 'has the first journal\'s creation time as the lower and the second journal\'s creation time ' \ - 'as the upper bound for validity_period of the first journal' do + it "has the first journal's creation time as the lower and the second journal's creation time " \ + "as the upper bound for validity_period of the first journal" do expect(subject.first.validity_period) .to eql(subject.first.created_at...subject.second.created_at) end - it 'has the second journal\'s creation time as the lower and no upper bound for validity_period of the second journal' do + it "has the second journal's creation time as the lower and no upper bound for validity_period of the second journal" do expect(subject.second.validity_period) .to eql(subject.second.created_at...) end end - context 'when updating with aggregation disabled', with_settings: { journal_aggregation_time_minutes: 0 } do + context "when updating with aggregation disabled", with_settings: { journal_aggregation_time_minutes: 0 } do subject(:journals) { work_package.journals } - context 'when WP updated within milliseconds' do + context "when WP updated within milliseconds" do before do work_package.status = build(:status) work_package.save! end - it 'creates a new journal' do + it "creates a new journal" do expect(journals.count).to be 2 end end end - context 'when aggregation leads to an empty change (changing back and forth)', + context "when aggregation leads to an empty change (changing back and forth)", with_settings: { journal_aggregation_time_minutes: 1 } do let!(:work_package) do User.execute_as current_user do @@ -832,7 +832,7 @@ created_at: 5.minutes.ago, project_id: project.id, type:, - description: 'Description', + description: "Description", priority:, status:, duration: 1) @@ -848,17 +848,17 @@ work_package.save! end - it 'creates a new journal' do + it "creates a new journal" do expect(work_package.journals.count).to be 2 end - it 'has the old state in the last journal`s data' do + it "has the old state in the last journal`s data" do expect(work_package.journals.last.data.status_id).to be status.id end end end - describe '#destroy' do + describe "#destroy" do let(:project) { create(:project) } let(:type) { create(:type) } let(:custom_field) do @@ -887,27 +887,27 @@ work_package.destroy end - it 'removes the journal' do + it "removes the journal" do expect(Journal.find_by(id: journal.id)) .to be_nil end - it 'removes the journal data' do + it "removes the journal data" do expect(Journal::WorkPackageJournal.find_by(id: journal.data_id)) .to be_nil end - it 'removes the customizable journals' do + it "removes the customizable journals" do expect(Journal::CustomizableJournal.find_by(id: customizable_journals.map(&:id))) .to be_nil end - it 'removes the attachable journals' do + it "removes the attachable journals" do expect(Journal::AttachableJournal.find_by(id: attachable_journals.map(&:id))) .to be_nil end - it 'removes the storable journals' do + it "removes the storable journals" do expect(Journal::StorableJournal.find_by(id: attachable_journals.map(&:id))) .to be_nil end diff --git a/spec/models/work_packages/scopes/allowed_to_spec.rb b/spec/models/work_packages/scopes/allowed_to_spec.rb index 7346616d0798..39f1c6e793e0 100644 --- a/spec/models/work_packages/scopes/allowed_to_spec.rb +++ b/spec/models/work_packages/scopes/allowed_to_spec.rb @@ -29,15 +29,14 @@ require "spec_helper" RSpec.describe WorkPackage, ".allowed_to" do - let(:user) { create(:user) } + shared_let(:user) { create(:user) } + shared_let(:project_status) { true } + shared_let(:private_project) { create(:project, public: false, active: project_status) } + shared_let(:public_project) { create(:project, public: true, active: project_status) } - let!(:private_project) { create(:project, public: false, active: project_status) } - let!(:public_project) { create(:project, public: true, active: project_status) } - let(:project_status) { true } - - let!(:work_package_in_public_project) { create(:work_package, project: public_project) } - let!(:work_package_in_private_project) { create(:work_package, project: private_project) } - let!(:other_work_package_in_private_project) { create(:work_package, project: private_project) } + shared_let(:work_package_in_public_project) { create(:work_package, project: public_project) } + shared_let(:work_package_in_private_project) { create(:work_package, project: private_project) } + shared_let(:other_work_package_in_private_project) { create(:work_package, project: private_project) } let(:project_permissions) { [] } let(:project_role) { create(:project_role, permissions: project_permissions) } @@ -106,6 +105,16 @@ expect(subject).to be_empty end end + + context "when the module the permission belongs to is disabled" do + before do + private_project.enabled_module_names = private_project.enabled_module_names - ["work_package_tracking"] + end + + it "excludes work packages where the module is disabled in" do + expect(subject).to contain_exactly(work_package_in_public_project) + end + end end context "when the user has the permission directly on the work package" do diff --git a/spec/requests/api/v3/activities_by_work_package_resource_spec.rb b/spec/requests/api/v3/activities_by_work_package_resource_spec.rb index eb8198d6c67c..18ae1c9fd0e0 100644 --- a/spec/requests/api/v3/activities_by_work_package_resource_spec.rb +++ b/spec/requests/api/v3/activities_by_work_package_resource_spec.rb @@ -52,14 +52,14 @@ end it "succeeds" do - expect(last_response.status).to be 200 + expect(last_response).to have_http_status :ok end context "not allowed to see work package" do let(:current_user) { create(:user) } it "fails with HTTP Not Found" do - expect(last_response.status).to be 404 + expect(last_response).to have_http_status :not_found end end end @@ -96,7 +96,7 @@ include_context "create activity" it "responds with error" do - expect(last_response.status).to be 422 + expect(last_response).to have_http_status :unprocessable_entity end it "notes the error" do diff --git a/spec/requests/api/v3/attachments_spec.rb b/spec/requests/api/v3/attachments_spec.rb index 3385e8b8dc9d..6fbd11c32b5a 100644 --- a/spec/requests/api/v3/attachments_spec.rb +++ b/spec/requests/api/v3/attachments_spec.rb @@ -55,7 +55,7 @@ let(:permissions) { [] } it "forbids to prepare attachments" do - expect(last_response.status).to eq 403 + expect(last_response).to have_http_status :forbidden end end @@ -63,7 +63,7 @@ let(:permissions) { [:edit_work_packages] } it "can prepare attachments" do - expect(last_response.status).to eq 201 + expect(last_response).to have_http_status :created end end @@ -71,7 +71,7 @@ let(:permissions) { [:add_work_package_attachments] } it "can prepare attachments" do - expect(last_response.status).to eq 201 + expect(last_response).to have_http_status :created end end end @@ -94,7 +94,7 @@ let(:status) { :uploaded } it "returns 404" do - expect(last_response.status).to eq 404 + expect(last_response).to have_http_status :not_found end end @@ -104,7 +104,7 @@ end it "responds with HTTP OK" do - expect(last_response.status).to eq 200 + expect(last_response).to have_http_status :ok end it "returns the attachment representation" do diff --git a/spec/requests/api/v3/authentication_spec.rb b/spec/requests/api/v3/authentication_spec.rb index 10ee53ab5769..ca642ee1d4c5 100644 --- a/spec/requests/api/v3/authentication_spec.rb +++ b/spec/requests/api/v3/authentication_spec.rb @@ -48,7 +48,7 @@ let(:oauth_access_token) { token.plaintext_token } it "authenticates successfully" do - expect(last_response.status).to eq 200 + expect(last_response).to have_http_status :ok end end @@ -56,7 +56,7 @@ let(:oauth_access_token) { "1337" } it "returns unauthorized" do - expect(last_response.status).to eq 401 + expect(last_response).to have_http_status :unauthorized end end @@ -65,7 +65,7 @@ let(:oauth_access_token) { token.plaintext_token } it "returns unauthorized" do - expect(last_response.status).to eq 401 + expect(last_response).to have_http_status :unauthorized end end end @@ -97,7 +97,7 @@ def set_basic_auth_header(user, password) end it "returns 401 unauthorized" do - expect(last_response.status).to eq 401 + expect(last_response).to have_http_status :unauthorized end end end @@ -109,7 +109,7 @@ def set_basic_auth_header(user, password) end it "returns 401 unauthorized" do - expect(last_response.status).to eq 401 + expect(last_response).to have_http_status :unauthorized end it "returns the correct JSON response" do @@ -131,7 +131,7 @@ def set_basic_auth_header(user, password) end it "returns 401 unauthorized" do - expect(last_response.status).to eq 401 + expect(last_response).to have_http_status :unauthorized end it "returns the correct JSON response" do @@ -156,7 +156,7 @@ def set_basic_auth_header(user, password) end it "returns 401 unauthorized" do - expect(last_response.status).to eq 401 + expect(last_response).to have_http_status :unauthorized end it "returns the correct JSON response" do @@ -183,7 +183,7 @@ def set_basic_auth_header(user, password) end it "returns 401 unauthorized" do - expect(last_response.status).to eq 401 + expect(last_response).to have_http_status :unauthorized end it "returns the correct JSON response" do @@ -207,7 +207,7 @@ def set_basic_auth_header(user, password) end it "returns 200 OK" do - expect(last_response.status).to eq 200 + expect(last_response).to have_http_status :ok end end end @@ -276,7 +276,7 @@ def set_basic_auth_header(user, password) end it "returns 200 OK" do - expect(last_response.status).to eq 200 + expect(last_response).to have_http_status :ok end it '"login"s the anonymous user' do @@ -291,7 +291,7 @@ def set_basic_auth_header(user, password) end it "returns 401 unauthorized" do - expect(last_response.status).to eq 401 + expect(last_response).to have_http_status :unauthorized end end @@ -302,7 +302,7 @@ def set_basic_auth_header(user, password) end it "returns 200 OK" do - expect(last_response.status).to eq 200 + expect(last_response).to have_http_status :ok end end @@ -313,7 +313,7 @@ def set_basic_auth_header(user, password) end it "returns 200 OK" do - expect(last_response.status).to eq 200 + expect(last_response).to have_http_status :ok end end end diff --git a/spec/requests/api/v3/backups/backups_api_spec.rb b/spec/requests/api/v3/backups/backups_api_spec.rb index dc964fc06b5d..190e0dd1df4c 100644 --- a/spec/requests/api/v3/backups/backups_api_spec.rb +++ b/spec/requests/api/v3/backups/backups_api_spec.rb @@ -59,7 +59,7 @@ def create_backup include_context "request" it "results in a bad request error" do - expect(last_response.status).to eq 400 + expect(last_response).to have_http_status :bad_request end end @@ -74,7 +74,7 @@ def create_backup end it "enqueues the backup including attachments" do - expect(last_response.status).to eq 202 + expect(last_response).to have_http_status :accepted end end @@ -91,7 +91,7 @@ def create_backup end it "enqueues a backup not including attachments" do - expect(last_response.status).to eq 202 + expect(last_response).to have_http_status :accepted end end end @@ -103,7 +103,7 @@ def create_backup include_context "request" it "results in a conflict" do - expect(last_response.status).to eq 409 + expect(last_response).to have_http_status :conflict end end @@ -113,7 +113,7 @@ def create_backup include_context "request" it "is forbidden" do - expect(last_response.status).to eq 403 + expect(last_response).to have_http_status :forbidden end end @@ -124,7 +124,7 @@ def create_backup include_context "request" it "is forbidden" do - expect(last_response.status).to eq 403 + expect(last_response).to have_http_status :forbidden end end @@ -132,7 +132,7 @@ def create_backup include_context "request" it "is rate limited" do - expect(last_response.status).to eq 429 + expect(last_response).to have_http_status :too_many_requests end end @@ -142,7 +142,7 @@ def create_backup include_context "request" it "is forbidden" do - expect(last_response.status).to eq 403 + expect(last_response).to have_http_status :forbidden end it "shows the remaining hours until the token is valid" do diff --git a/spec/requests/api/v3/category_resource_spec.rb b/spec/requests/api/v3/category_resource_spec.rb index b4ad3aaea0eb..b994ec4ce772 100644 --- a/spec/requests/api/v3/category_resource_spec.rb +++ b/spec/requests/api/v3/category_resource_spec.rb @@ -100,10 +100,7 @@ context "invalid priority id" do let(:get_path) { api_v3_paths.category "bogus" } - it_behaves_like "param validation error" do - let(:id) { "bogus" } - let(:type) { "Category" } - end + it_behaves_like "not found" end end @@ -116,10 +113,7 @@ get get_path end - it_behaves_like "param validation error" do - let(:id) { "bogus" } - let(:type) { "Category" } - end + it_behaves_like "not found" end end end diff --git a/spec/requests/api/v3/content_type_header_spec.rb b/spec/requests/api/v3/content_type_header_spec.rb index 5bd99118355a..28c40942752f 100644 --- a/spec/requests/api/v3/content_type_header_spec.rb +++ b/spec/requests/api/v3/content_type_header_spec.rb @@ -50,7 +50,7 @@ context "on a GET request" do it "is successful" do get api_v3_paths.work_package(work_package.id) - expect(last_response.status).not_to eq(406) + expect(last_response.status).not_to have_http_status(:not_acceptable) expect(last_response).to be_ok end end @@ -58,7 +58,7 @@ context "on a DELETE request" do it "is successful" do delete api_v3_paths.work_package(work_package.id) - expect(last_response.status).not_to eq(406) + expect(last_response.status).not_to have_http_status(:not_acceptable) expect(last_response).to be_no_content end end @@ -66,7 +66,7 @@ context "on any other HTTP method" do it "responds with a 406 status and a missing Content-Type header message" do patch api_v3_paths.work_package(work_package.id), {} - expect(last_response.status).to eq(406) + expect(last_response).to have_http_status(:not_acceptable) expect(last_response.body).to include("Missing content-type header") end end diff --git a/spec/requests/api/v3/groups/group_resource_spec.rb b/spec/requests/api/v3/groups/group_resource_spec.rb index 4d6619fe9b01..218aa13f056a 100644 --- a/spec/requests/api/v3/groups/group_resource_spec.rb +++ b/spec/requests/api/v3/groups/group_resource_spec.rb @@ -122,7 +122,7 @@ current_user { create(:admin) } it "responds with 201" do - expect(last_response.status).to eq(201) + expect(last_response).to have_http_status(:created) end it "creates the group and sets the members" do @@ -155,7 +155,7 @@ end it "responds with 422 and explains the error" do - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) expect(last_response.body) .to be_json_eql("Name can't be blank.".to_json) @@ -221,7 +221,7 @@ current_user { admin } it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "updates the group" do diff --git a/spec/requests/api/v3/help_texts/help_texts_resource_spec.rb b/spec/requests/api/v3/help_texts/help_texts_resource_spec.rb index 55acc25f70d3..cccdb84c9836 100644 --- a/spec/requests/api/v3/help_texts/help_texts_resource_spec.rb +++ b/spec/requests/api/v3/help_texts/help_texts_resource_spec.rb @@ -86,16 +86,13 @@ end context "valid type id" do - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } end context "invalid type id" do let(:get_path) { api_v3_paths.type "bogus" } - it_behaves_like "param validation error" do - let(:id) { "bogus" } - let(:type) { "HelpText" } - end + it_behaves_like "not found" end context "invisible type id" do diff --git a/spec/requests/api/v3/membership_resources_spec.rb b/spec/requests/api/v3/membership_resources_spec.rb index 1a64fbfb490e..d2597ca63e40 100644 --- a/spec/requests/api/v3/membership_resources_spec.rb +++ b/spec/requests/api/v3/membership_resources_spec.rb @@ -438,7 +438,7 @@ let(:role) { defined?(expected_role) ? expected_role : other_role } it "responds with 201" do - expect(last_response.status).to eq(201) + expect(last_response).to have_http_status(:created) end it "creates the member" do @@ -675,7 +675,7 @@ context "as a non admin" do it "responds with 422 and explains the error" do - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) expect(last_response.body) .to be_json_eql("Project can't be blank.".to_json) @@ -705,7 +705,7 @@ end it "responds with 422 and explains the error" do - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) expect(last_response.body) .to be_json_eql("User has already been taken.".to_json) @@ -734,7 +734,7 @@ end it "responds with 422 and explains the error" do - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) error_message = "For property 'user' a link like '/api/v3/groups/:id' or " + "'/api/v3/users/:id' or '/api/v3/placeholder_users/:id' is expected, " + @@ -762,7 +762,7 @@ end it "responds with 422 and explains the error" do - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) expect(last_response.body) .to be_json_eql("Roles need to be assigned.".to_json) @@ -862,7 +862,7 @@ context "for a user" do it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "updates the member" do @@ -955,7 +955,7 @@ let(:last_user_member_updated_at) { Member.find_by(principal: users.last).updated_at } it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "updates the member and all inherited members but does not update memberships users have already had" do @@ -1038,7 +1038,7 @@ let(:another_role) { create(:global_role) } it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "updates the member and all inherited members but does not update memberships users have already had" do diff --git a/spec/requests/api/v3/memberships/create_form_resource_spec.rb b/spec/requests/api/v3/memberships/create_form_resource_spec.rb index 20e10935cebd..51e8128ebac4 100644 --- a/spec/requests/api/v3/memberships/create_form_resource_spec.rb +++ b/spec/requests/api/v3/memberships/create_form_resource_spec.rb @@ -53,7 +53,7 @@ describe "#POST /api/v3/memberships/form" do it "returns 200 OK" do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) end it "returns a form" do @@ -157,7 +157,7 @@ let(:permissions) { [] } it "returns 403 Not Authorized" do - expect(response.status).to eq(403) + expect(response).to have_http_status(:forbidden) end end end diff --git a/spec/requests/api/v3/memberships/update_form_resource_spec.rb b/spec/requests/api/v3/memberships/update_form_resource_spec.rb index fbb950b1ced6..ffcbcf508e3e 100644 --- a/spec/requests/api/v3/memberships/update_form_resource_spec.rb +++ b/spec/requests/api/v3/memberships/update_form_resource_spec.rb @@ -73,7 +73,7 @@ describe "#POST /api/v3/memberships/:id/form" do it "returns 200 OK" do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) end it "returns a form" do @@ -217,7 +217,7 @@ let(:permissions) { [:view_members] } it "returns 403 Not Authorized" do - expect(response.status).to eq(403) + expect(response).to have_http_status(:forbidden) end end @@ -225,7 +225,7 @@ let(:permissions) { [] } it "returns 404 Not Found" do - expect(response.status).to eq(404) + expect(response).to have_http_status(:not_found) end end end diff --git a/spec/requests/api/v3/news/create_shared_examples.rb b/spec/requests/api/v3/news/create_shared_examples.rb new file mode 100644 index 000000000000..b8015d7de0f0 --- /dev/null +++ b/spec/requests/api/v3/news/create_shared_examples.rb @@ -0,0 +1,94 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. + +RSpec.shared_context "create news request context" do + include API::V3::Utilities::PathHelper + + let(:parameters) do + { + title: "My news entry", + summary: "Hello from API", + _links: { + project: { + href: api_v3_paths.project(project.id) + } + } + } + end + + let(:send_request) do + header "Content-Type", "application/json" + post api_v3_paths.newses, parameters.to_json + end + + let(:parsed_response) { JSON.parse(last_response.body) } +end + +RSpec.shared_examples "create news request flow" do + include_context "create news request context" + + describe "empty request body" do + let(:parameters) { {} } + + it "returns an erroneous response" do + send_request + + expect(last_response.status).to eq(422) + expect(last_response.body) + .to be_json_eql("urn:openproject-org:api:v3:errors:MultipleErrors".to_json) + .at_path("errorIdentifier") + end + end + + it "creates the news when valid" do + send_request + + expect(last_response.status).to eq(201) + news = News.find_by(title: parameters[:title]) + expect(news).to be_present + expect(news.project).to eq(project) + expect(news.author).to eq(user) + end + + describe "when the title is missing" do + it "returns an error" do + header "Content-Type", "application/json" + post api_v3_paths.newses, parameters.except(:title).to_json + + expect(last_response.status).to eq(422) + expect(last_response.body) + .to be_json_eql("urn:openproject-org:api:v3:errors:PropertyConstraintViolation".to_json) + .at_path("errorIdentifier") + + expect(parsed_response["_embedded"]["details"]["attribute"]) + .to eq "title" + + expect(parsed_response["message"]) + .to eq "Title can't be blank." + end + end +end diff --git a/spec/requests/api/v3/news/index_resource_spec.rb b/spec/requests/api/v3/news/index_resource_spec.rb new file mode 100644 index 000000000000..7065f5d04827 --- /dev/null +++ b/spec/requests/api/v3/news/index_resource_spec.rb @@ -0,0 +1,80 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. + +require "spec_helper" +require "rack/test" + +RSpec.describe API::V3::News::NewsAPI, + "index", + content_type: :json do + include API::V3::Utilities::PathHelper + + shared_let(:project1) { create(:project) } + shared_let(:project2) { create(:project) } + shared_let(:news1) { create(:news, project: project1) } + shared_let(:news2) { create(:news, project: project2) } + + let(:send_request) do + get api_v3_paths.newses + end + + let(:parsed_response) { JSON.parse(last_response.body) } + + current_user { user } + + before do + send_request + end + + context "for an admin user" do + let(:user) { build(:admin) } + + it_behaves_like "API V3 collection response", 2, 2, "News" + end + + context "for a user with view_news permissions in one project" do + let(:user) { create(:user, member_with_permissions: { project1 => %i[view_news]}) } + + it_behaves_like "API V3 collection response", 1, 1, "News" + + it "returns only the news in the visible project" do + expect(last_response.body) + .to be_json_eql(api_v3_paths.news(news1.id).to_json) + .at_path("_embedded/elements/0/_links/self/href") + + expect(last_response.body) + .to be_json_eql(api_v3_paths.project(project1.id).to_json) + .at_path("_embedded/elements/0/_links/project/href") + end + end + + context "for an unauthorized user" do + let(:user) { build(:user) } + + it_behaves_like "API V3 collection response", 0, 0, "News" + end +end diff --git a/spec/requests/api/v3/news/news_api_create_spec.rb b/spec/requests/api/v3/news/news_api_create_spec.rb new file mode 100644 index 000000000000..b49c8dfb7809 --- /dev/null +++ b/spec/requests/api/v3/news/news_api_create_spec.rb @@ -0,0 +1,58 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. + +require "spec_helper" +require "rack/test" +require_relative "create_shared_examples" + +RSpec.describe API::V3::News::NewsAPI, "create" do + include_context "create news request context" + shared_let(:project) { create(:project, enabled_module_names: %w[news]) } + current_user { user } + + describe "admin user" do + let(:user) { build(:admin) } + + it_behaves_like "create news request flow" + end + + describe "user with manage_news permission" do + let(:user) { create(:user, member_with_permissions: { project => %i[view_news manage_news] }) } + + it_behaves_like "create news request flow" + end + + describe "unauthorized user" do + let(:user) { create(:user, member_with_permissions: { project => %i[view_news] }) } + + it "returns an erroneous response" do + send_request + + expect(last_response.status).to eq(403) + end + end +end diff --git a/spec/requests/api/v3/news/news_api_delete_spec.rb b/spec/requests/api/v3/news/news_api_delete_spec.rb new file mode 100644 index 000000000000..fd0fa2f59593 --- /dev/null +++ b/spec/requests/api/v3/news/news_api_delete_spec.rb @@ -0,0 +1,108 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. + +require "spec_helper" + +RSpec.describe API::V3::News::NewsAPI, "delete" do + include API::V3::Utilities::PathHelper + + shared_let(:project) { create(:project) } + shared_let(:news) { create(:news, project:, title: "foo") } + + let(:send_request) do + header "Content-Type", "application/json" + end + + let(:path) { api_v3_paths.news(news.id) } + let(:parsed_response) { JSON.parse(last_response.body) } + + current_user { user } + + RSpec.shared_examples "deletion allowed" do + it "deletes the news" do + expect(last_response.status).to eq 204 + expect { news.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + context "with a non-existent news" do + let(:path) { api_v3_paths.news 1337 } + + it_behaves_like "not found" + end + end + + RSpec.shared_examples "deletion is not allowed" do |status| + it "does not delete the user" do + expect(last_response.status).to eq status + expect(News).to exist(news.id) + end + end + + before do + header "Content-Type", "application/json" + delete path + end + + context "when admin" do + let(:user) { build_stubbed(:admin) } + + it_behaves_like "deletion allowed" + end + + context "when locked admin" do + let(:user) { build_stubbed(:admin, status: Principal.statuses[:locked]) } + + it_behaves_like "deletion is not allowed", 404 + end + + context "when non-admin" do + let(:user) { build_stubbed(:user, admin: false) } + + it_behaves_like "deletion is not allowed", 404 + end + + context "when user with manage_news permission" do + let(:user) { create(:user, member_with_permissions: { project => %i[view_news manage_news] }) } + + it_behaves_like "deletion allowed" + end + + context "when anonymous user" do + let(:user) { create(:anonymous) } + + context "when login_required", with_settings: { login_required: true } do + it_behaves_like "error response", + 401, + "Unauthenticated", + I18n.t("api_v3.errors.code_401") + end + + context "when not login_required", with_settings: { login_required: false } do + it_behaves_like "deletion is not allowed", 404 + end + end +end diff --git a/spec/requests/api/v3/news/show_resource_examples.rb b/spec/requests/api/v3/news/show_resource_examples.rb new file mode 100644 index 000000000000..49ce4544beae --- /dev/null +++ b/spec/requests/api/v3/news/show_resource_examples.rb @@ -0,0 +1,42 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. + +RSpec.shared_examples "represents the news" do + it do + aggregate_failures do + expect(last_response.status).to eq(200) + expect(last_response.body) + .to(be_json_eql("News".to_json).at_path("_type")) + + expect(last_response.body) + .to(be_json_eql(news.title.to_json).at_path("title")) + + expect(last_response.body) + .to(be_json_eql(news.id.to_json).at_path("id")) + end + end +end diff --git a/spec/requests/api/v3/news/show_resource_spec.rb b/spec/requests/api/v3/news/show_resource_spec.rb new file mode 100644 index 000000000000..63c5638ab530 --- /dev/null +++ b/spec/requests/api/v3/news/show_resource_spec.rb @@ -0,0 +1,70 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. + +require "spec_helper" +require_relative "show_resource_examples" + +RSpec.describe API::V3::News::NewsAPI, + "show" do + include API::V3::Utilities::PathHelper + + shared_let(:project) { create(:project) } + shared_let(:news) { create(:news, title: "foo", project:) } + + let(:send_request) do + header "Content-Type", "application/json" + get api_v3_paths.news(news.id) + end + + let(:parsed_response) { JSON.parse(last_response.body) } + + current_user { user } + + before do + send_request + end + + describe "admin user" do + let(:user) { build(:admin) } + + it_behaves_like "represents the news" + end + + describe "user with manage_news_user permission" do + let(:user) { create(:user, member_with_permissions: { project => %i[view_news] }) } + + it_behaves_like "represents the news" + end + + describe "unauthorized user" do + let(:user) { build(:user) } + + it "returns a 404 response" do + expect(last_response.status).to eq(404) + end + end +end diff --git a/spec/requests/api/v3/news/update_resource_examples.rb b/spec/requests/api/v3/news/update_resource_examples.rb new file mode 100644 index 000000000000..3fbbca965b39 --- /dev/null +++ b/spec/requests/api/v3/news/update_resource_examples.rb @@ -0,0 +1,61 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. + +RSpec.shared_examples "updates the news" do + context "with an empty title" do + let(:parameters) do + { title: "" } + end + + it "returns an error" do + expect(last_response.status).to eq(422) + expect(last_response.body) + .to be_json_eql("urn:openproject-org:api:v3:errors:PropertyConstraintViolation".to_json) + .at_path("errorIdentifier") + + expect(parsed_response["_embedded"]["details"]["attribute"]) + .to eq "title" + + expect(parsed_response["message"]) + .to eq "Title can't be blank." + end + end + + context "with a new title" do + let(:parameters) do + { title: "my new title" } + end + + it "updates the news" do + expect(last_response.status).to eq(200) + + news.reload + + expect(news.title).to eq "my new title" + end + end +end diff --git a/spec/requests/api/v3/news/update_resource_spec.rb b/spec/requests/api/v3/news/update_resource_spec.rb new file mode 100644 index 000000000000..f2fe8f97684f --- /dev/null +++ b/spec/requests/api/v3/news/update_resource_spec.rb @@ -0,0 +1,74 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. + +require "spec_helper" +require_relative "update_resource_examples" + +RSpec.describe API::V3::News::NewsAPI, + "update" do + include API::V3::Utilities::PathHelper + + shared_let(:project) { create(:project) } + shared_let(:news) { create(:news, title: "foo", project:) } + + let(:parameters) do + {} + end + + let(:send_request) do + header "Content-Type", "application/json" + patch api_v3_paths.news(news.id), parameters.to_json + end + + let(:parsed_response) { JSON.parse(last_response.body) } + + current_user { user } + + before do + send_request + end + + describe "admin user" do + let(:user) { create(:admin) } + + it_behaves_like "updates the news" + end + + describe "user with manage_news permission" do + let(:user) { create(:user, member_with_permissions: { project => %i[view_news manage_news] }) } + + it_behaves_like "updates the news" + end + + describe "unauthorized user" do + let(:user) { build(:user) } + + it "returns a 404 response" do + expect(last_response.status).to eq(404) + end + end +end diff --git a/spec/requests/api/v3/notifications/details_resource_spec.rb b/spec/requests/api/v3/notifications/details_resource_spec.rb index 81a0901514b0..e374c098cabf 100644 --- a/spec/requests/api/v3/notifications/details_resource_spec.rb +++ b/spec/requests/api/v3/notifications/details_resource_spec.rb @@ -77,7 +77,7 @@ it "returns a 404 response" do send_request - expect(last_response.status).to eq(404) + expect(last_response).to have_http_status(:not_found) end end @@ -151,7 +151,7 @@ end it "returns a 404 response" do - expect(last_response.status).to eq(404) + expect(last_response).to have_http_status(:not_found) end end @@ -163,7 +163,7 @@ end it "returns a 404 response" do - expect(last_response.status).to eq(404) + expect(last_response).to have_http_status(:not_found) end end end diff --git a/spec/requests/api/v3/notifications/read_ian_resource_spec.rb b/spec/requests/api/v3/notifications/read_ian_resource_spec.rb index 696cd8eba49f..cd63f2feba7e 100644 --- a/spec/requests/api/v3/notifications/read_ian_resource_spec.rb +++ b/spec/requests/api/v3/notifications/read_ian_resource_spec.rb @@ -64,11 +64,11 @@ it "can read and unread" do send_read - expect(last_response.status).to eq(204) + expect(last_response).to have_http_status(:no_content) expect(notification.reload.read_ian).to be_truthy send_unread - expect(last_response.status).to eq(204) + expect(last_response).to have_http_status(:no_content) expect(notification.reload.read_ian).to be_falsey end end @@ -78,10 +78,10 @@ it "returns a 404 response" do send_read - expect(last_response.status).to eq(404) + expect(last_response).to have_http_status(:not_found) send_unread - expect(last_response.status).to eq(404) + expect(last_response).to have_http_status(:not_found) end end end diff --git a/spec/requests/api/v3/notifications/show_resource_spec.rb b/spec/requests/api/v3/notifications/show_resource_spec.rb index c12bce676fa3..82a8310bfa14 100644 --- a/spec/requests/api/v3/notifications/show_resource_spec.rb +++ b/spec/requests/api/v3/notifications/show_resource_spec.rb @@ -99,7 +99,7 @@ end it "returns a 404 response" do - expect(last_response.status).to eq(404) + expect(last_response).to have_http_status(:not_found) end end @@ -111,7 +111,7 @@ end it "returns a 404 response" do - expect(last_response.status).to eq(404) + expect(last_response).to have_http_status(:not_found) end end end diff --git a/spec/requests/api/v3/placeholder_users/create_resource_spec.rb b/spec/requests/api/v3/placeholder_users/create_resource_spec.rb index faefd36171b8..0360cbe4000e 100644 --- a/spec/requests/api/v3/placeholder_users/create_resource_spec.rb +++ b/spec/requests/api/v3/placeholder_users/create_resource_spec.rb @@ -51,7 +51,7 @@ it "returns an erroneous response" do send_request - expect(last_response.status).to eq(403) + expect(last_response).to have_http_status(:forbidden) end end end diff --git a/spec/requests/api/v3/placeholder_users/create_shared_examples.rb b/spec/requests/api/v3/placeholder_users/create_shared_examples.rb index d0827cd509be..32ed68004b2c 100644 --- a/spec/requests/api/v3/placeholder_users/create_shared_examples.rb +++ b/spec/requests/api/v3/placeholder_users/create_shared_examples.rb @@ -50,7 +50,7 @@ it "returns an erroneous response" do send_request - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) expect(last_response.body) .to be_json_eql("urn:openproject-org:api:v3:errors:PropertyConstraintViolation".to_json) .at_path("errorIdentifier") @@ -63,7 +63,7 @@ it "creates the placeholder when valid" do send_request - expect(last_response.status).to eq(201) + expect(last_response).to have_http_status(:created) placeholder = PlaceholderUser.find_by(name: parameters[:name]) expect(placeholder).to be_present end @@ -74,7 +74,7 @@ it "returns an error" do send_request - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) expect(last_response.body) .to be_json_eql("urn:openproject-org:api:v3:errors:PropertyConstraintViolation".to_json) .at_path("errorIdentifier") @@ -92,7 +92,7 @@ it "adds an error that its only available in EE" do send_request - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) expect(parsed_response["message"]) .to eq("Placeholder Users is only available in the OpenProject Enterprise edition") diff --git a/spec/requests/api/v3/placeholder_users/delete_resource_examples.rb b/spec/requests/api/v3/placeholder_users/delete_resource_examples.rb index 185e6efc7f43..1df31faf3de8 100644 --- a/spec/requests/api/v3/placeholder_users/delete_resource_examples.rb +++ b/spec/requests/api/v3/placeholder_users/delete_resource_examples.rb @@ -27,7 +27,7 @@ RSpec.shared_examples "deletion allowed" do it "responds with 202" do - expect(last_response.status).to eq 202 + expect(last_response).to have_http_status :accepted end it "locks the account and mark for deletion" do @@ -47,7 +47,7 @@ RSpec.shared_examples "deletion is not allowed" do it "responds with 403" do - expect(last_response.status).to eq 403 + expect(last_response).to have_http_status :forbidden end it "does not delete the user" do diff --git a/spec/requests/api/v3/placeholder_users/show_resource_examples.rb b/spec/requests/api/v3/placeholder_users/show_resource_examples.rb index 1a7f451172ec..1d4da78c9c7f 100644 --- a/spec/requests/api/v3/placeholder_users/show_resource_examples.rb +++ b/spec/requests/api/v3/placeholder_users/show_resource_examples.rb @@ -27,7 +27,7 @@ RSpec.shared_examples "represents the placeholder" do it do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) expect(last_response.body) .to(be_json_eql("PlaceholderUser".to_json).at_path("_type")) diff --git a/spec/requests/api/v3/placeholder_users/show_resource_spec.rb b/spec/requests/api/v3/placeholder_users/show_resource_spec.rb index 3a4cd43697cb..2f598b749a02 100644 --- a/spec/requests/api/v3/placeholder_users/show_resource_spec.rb +++ b/spec/requests/api/v3/placeholder_users/show_resource_spec.rb @@ -71,7 +71,7 @@ let(:user) { build(:user) } it "returns a 403 response" do - expect(last_response.status).to eq(403) + expect(last_response).to have_http_status(:forbidden) end end end diff --git a/spec/requests/api/v3/placeholder_users/update_resource_examples.rb b/spec/requests/api/v3/placeholder_users/update_resource_examples.rb index b6feb8996cce..83fcfffb967c 100644 --- a/spec/requests/api/v3/placeholder_users/update_resource_examples.rb +++ b/spec/requests/api/v3/placeholder_users/update_resource_examples.rb @@ -32,7 +32,7 @@ end it "returns an error" do - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) expect(last_response.body) .to be_json_eql("urn:openproject-org:api:v3:errors:PropertyConstraintViolation".to_json) .at_path("errorIdentifier") @@ -51,7 +51,7 @@ end it "updates the placeholder" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) placeholder.reload diff --git a/spec/requests/api/v3/placeholder_users/update_resource_spec.rb b/spec/requests/api/v3/placeholder_users/update_resource_spec.rb index ecd50789329e..356764e5422d 100644 --- a/spec/requests/api/v3/placeholder_users/update_resource_spec.rb +++ b/spec/requests/api/v3/placeholder_users/update_resource_spec.rb @@ -67,7 +67,7 @@ let(:user) { build(:user) } it "returns a 403 response" do - expect(last_response.status).to eq(403) + expect(last_response).to have_http_status(:forbidden) end end end diff --git a/spec/requests/api/v3/priority_resource_spec.rb b/spec/requests/api/v3/priority_resource_spec.rb index 38547bf77dc4..b7f93bdb39bf 100644 --- a/spec/requests/api/v3/priority_resource_spec.rb +++ b/spec/requests/api/v3/priority_resource_spec.rb @@ -86,10 +86,7 @@ context "invalid priority id" do let(:get_path) { api_v3_paths.priority "bogus" } - it_behaves_like "param validation error" do - let(:id) { "bogus" } - let(:type) { "IssuePriority" } - end + it_behaves_like "not found" end end diff --git a/spec/requests/api/v3/projects/copy/copy_form_resource_spec.rb b/spec/requests/api/v3/projects/copy/copy_form_resource_spec.rb index 1f4ccd4df231..d1579e024e67 100644 --- a/spec/requests/api/v3/projects/copy/copy_form_resource_spec.rb +++ b/spec/requests/api/v3/projects/copy/copy_form_resource_spec.rb @@ -66,7 +66,7 @@ subject(:response) { last_response } it "returns 200 FORM response", :aggregate_failures do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) expect(response.body) .to be_json_eql("Form".to_json) @@ -240,7 +240,7 @@ end it "returns 403 Not Authorized" do - expect(response.status).to eq(403) + expect(response).to have_http_status(:forbidden) end end end diff --git a/spec/requests/api/v3/projects/copy/copy_resource_spec.rb b/spec/requests/api/v3/projects/copy/copy_resource_spec.rb index c5b528a3bb15..9ab848034167 100644 --- a/spec/requests/api/v3/projects/copy/copy_resource_spec.rb +++ b/spec/requests/api/v3/projects/copy/copy_resource_spec.rb @@ -71,12 +71,11 @@ subject(:response) { last_response } - # rubocop:disable RSpecRails/HaveHttpStatus # those are mock responses that don't deal well with the rails helpers describe "#POST /api/v3/projects/:id/copy" do describe "with empty params" do it "returns 422", :aggregate_failures do - expect(response.status).to eq(422) + expect(response).to have_http_status(:unprocessable_entity) expect(response.body) .to be_json_eql("Error".to_json) @@ -99,7 +98,7 @@ it "returns with a redirect to job" do aggregate_failures do - expect(response.status).to eq(302) + expect(response).to have_http_status(:found) expect(response).to be_redirect @@ -108,7 +107,7 @@ get response.location - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) expect(last_response.body) .to be_json_eql("in_queue".to_json) @@ -118,7 +117,7 @@ get response.location - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) expect(last_response.body) .to be_json_eql("success".to_json) @@ -199,9 +198,8 @@ end it "returns 403 Not Authorized" do - expect(response.status).to eq(403) + expect(response).to have_http_status(:forbidden) end end end - # rubocop:enable RSpecRails/HaveHttpStatus end diff --git a/spec/requests/api/v3/projects/create_form_resource_spec.rb b/spec/requests/api/v3/projects/create_form_resource_spec.rb index c92041ef0dcd..0654d6e40c3b 100644 --- a/spec/requests/api/v3/projects/create_form_resource_spec.rb +++ b/spec/requests/api/v3/projects/create_form_resource_spec.rb @@ -63,7 +63,7 @@ describe "#POST /api/v3/projects/form" do it "returns 200 OK" do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) end it "returns a form" do @@ -195,7 +195,7 @@ end it "returns 200 OK" do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) end it "returns the schema with a required parent field" do @@ -209,7 +209,7 @@ let(:permissions) { [] } it "returns 403 Not Authorized" do - expect(response.status).to eq(403) + expect(response).to have_http_status(:forbidden) end end end diff --git a/spec/requests/api/v3/projects/create_resource_spec.rb b/spec/requests/api/v3/projects/create_resource_spec.rb index 02106e9baa65..82836ddb59f9 100644 --- a/spec/requests/api/v3/projects/create_resource_spec.rb +++ b/spec/requests/api/v3/projects/create_resource_spec.rb @@ -63,7 +63,7 @@ end it "responds with 201 CREATED" do - expect(last_response.status).to eq(201) + expect(last_response).to have_http_status(:created) end it "creates a project" do @@ -182,7 +182,7 @@ let(:permissions) { [] } it "responds with 403" do - expect(last_response.status).to eq(403) + expect(last_response).to have_http_status(:forbidden) end it "creates no project" do @@ -199,7 +199,7 @@ end it "responds with 422" do - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) end it "creates no project" do @@ -233,7 +233,7 @@ end it "responds with 422" do - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) end it "creates no project" do diff --git a/spec/requests/api/v3/projects/index_resource_spec.rb b/spec/requests/api/v3/projects/index_resource_spec.rb index 7f074ffc8256..1161f999ce02 100644 --- a/spec/requests/api/v3/projects/index_resource_spec.rb +++ b/spec/requests/api/v3/projects/index_resource_spec.rb @@ -320,7 +320,7 @@ current_user { admin } it "responds with 200 OK" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it_behaves_like "API V3 collection response", 1, 1, "Project" do @@ -332,7 +332,7 @@ it_behaves_like "API V3 collection response", 0, 0, "Project" it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end end end diff --git a/spec/requests/api/v3/projects/update_form_resource_spec.rb b/spec/requests/api/v3/projects/update_form_resource_spec.rb index 490af1651edb..e2834bc408cd 100644 --- a/spec/requests/api/v3/projects/update_form_resource_spec.rb +++ b/spec/requests/api/v3/projects/update_form_resource_spec.rb @@ -74,7 +74,7 @@ describe "#POST /api/v3/projects/:id/form" do it "returns 200 OK" do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) end it "returns a form" do @@ -262,7 +262,7 @@ let(:permissions) { [] } it "returns 403 Not Authorized" do - expect(response.status).to eq(403) + expect(response).to have_http_status(:forbidden) end end @@ -270,7 +270,7 @@ let(:path) { api_v3_paths.project_form(1) } it "returns 404 Not found" do - expect(response.status).to eq(404) + expect(response).to have_http_status(:not_found) end end end diff --git a/spec/requests/api/v3/projects/update_resource_spec.rb b/spec/requests/api/v3/projects/update_resource_spec.rb index b9aa431487a1..26d72f607cdc 100644 --- a/spec/requests/api/v3/projects/update_resource_spec.rb +++ b/spec/requests/api/v3/projects/update_resource_spec.rb @@ -65,7 +65,7 @@ end it "responds with 200 OK" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "alters the project" do @@ -97,7 +97,7 @@ end it "responds with 200 OK" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "sets the cf value" do @@ -124,7 +124,7 @@ let(:current_user) { create(:admin) } it "responds with 200 OK" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "sets the cf value" do @@ -141,7 +141,7 @@ context "with non-admin permissions" do it "responds with 200 OK" do # TBD: trying to set a not accessible custom field is silently ignored - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "does not set the cf value" do @@ -171,7 +171,7 @@ let(:permissions) { [] } it "responds with 403" do - expect(last_response.status).to eq(403) + expect(last_response).to have_http_status(:forbidden) end it "does not change the project" do @@ -266,7 +266,7 @@ end it "responds with 422" do - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) end it "does not change the project" do @@ -299,7 +299,7 @@ end it "responds with 422" do - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) end it "does not change the project status" do diff --git a/spec/requests/api/v3/queries/create_form_api_spec.rb b/spec/requests/api/v3/queries/create_form_api_spec.rb index 3c08545f59c8..6036ca34397c 100644 --- a/spec/requests/api/v3/queries/create_form_api_spec.rb +++ b/spec/requests/api/v3/queries/create_form_api_spec.rb @@ -100,7 +100,7 @@ end it "returns 200(OK)" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "is of type form" do diff --git a/spec/requests/api/v3/queries/create_query_spec.rb b/spec/requests/api/v3/queries/create_query_spec.rb index ebd7719b3ced..8337d60091d0 100644 --- a/spec/requests/api/v3/queries/create_query_spec.rb +++ b/spec/requests/api/v3/queries/create_query_spec.rb @@ -110,7 +110,7 @@ def json end it "returns 201 (created)" do - expect(last_response.status).to eq(201) + expect(last_response).to have_http_status(:created) end it "renders the created query" do @@ -139,7 +139,7 @@ def json context "without EE", with_ee: false do it "yields a 422 error given a timestamp older than 1 day" do - expect(last_response.status).to eq 422 + expect(last_response).to have_http_status :unprocessable_entity expect(json["message"]).to eq "Timestamps contain forbidden values: #{timestamps.first}" end @@ -147,7 +147,7 @@ def json let(:timestamps) { ["oneDayAgo@12:00+00:00"] } it "returns 201 (created)" do - expect(last_response.status).to eq(201) + expect(last_response).to have_http_status(:created) end it "updates the query timestamps" do @@ -168,7 +168,7 @@ def post! post! - expect(last_response.status).to eq 422 + expect(last_response).to have_http_status :unprocessable_entity expect(json["message"]).to eq "Project not found" end @@ -177,7 +177,7 @@ def post! post! - expect(last_response.status).to eq 422 + expect(last_response).to have_http_status :unprocessable_entity expect(json["message"]).to eq "Status Operator is not set to one of the allowed values." end @@ -186,7 +186,7 @@ def post! post! - expect(last_response.status).to eq 422 + expect(last_response).to have_http_status :unprocessable_entity expect(json["message"]).to eq "Statuz filter does not exist." end end diff --git a/spec/requests/api/v3/queries/order/query_order_api_spec.rb b/spec/requests/api/v3/queries/order/query_order_api_spec.rb index 680d69beffb0..19967d7921a7 100644 --- a/spec/requests/api/v3/queries/order/query_order_api_spec.rb +++ b/spec/requests/api/v3/queries/order/query_order_api_spec.rb @@ -52,7 +52,7 @@ it "returns the order" do get path - expect(last_response.status).to eq 200 + expect(last_response).to have_http_status :ok expect(body).to be_a Hash expect(body).to eq({ wp1.id => 0, wp2.id => 8192 }.stringify_keys) end @@ -70,7 +70,7 @@ it "allows inserting a delta" do patch path, { delta: { wp2.id.to_s => 1234 } }.to_json - expect(last_response.status).to eq 200 + expect(last_response).to have_http_status :ok query.reload expect(body).to eq("t" => timestamp) @@ -79,7 +79,7 @@ it "allows removing an item" do patch path, { delta: { wp1.id.to_s => -1 } }.to_json - expect(last_response.status).to eq 200 + expect(last_response).to have_http_status :ok query.reload expect(body).to eq("t" => timestamp) diff --git a/spec/requests/api/v3/queries/query_resource_spec.rb b/spec/requests/api/v3/queries/query_resource_spec.rb index 1124cdb24ce1..26f7d345928c 100644 --- a/spec/requests/api/v3/queries/query_resource_spec.rb +++ b/spec/requests/api/v3/queries/query_resource_spec.rb @@ -65,7 +65,7 @@ context "user has view_work_packages in a project" do it "succeeds" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end end @@ -73,7 +73,7 @@ let(:permissions) { [:manage_public_queries] } it "succeeds" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end end @@ -89,7 +89,7 @@ include_context "with non-member permissions from non_member_permissions" it "succeeds" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end context "that is not allowed to see queries anywhere" do @@ -247,7 +247,7 @@ end it "responds with HTTP No Content" do - expect(last_response.status).to eq 204 + expect(last_response).to have_http_status :no_content end it "deletes the Query" do @@ -279,7 +279,7 @@ end it "succeeds" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "returns a Collection of projects for which the user has view work packages permission" do @@ -317,7 +317,7 @@ context "when starring an unstarred query" do it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it 'returns the query with "starred" property set to true' do @@ -327,7 +327,7 @@ context "when starring already starred query" do it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it 'returns the query with "starred" property set to true' do @@ -356,7 +356,7 @@ context "starring his own query" do it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it 'returns the query with "starred" property set to true' do @@ -398,7 +398,7 @@ end it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it 'returns the query with "starred" property set to false' do @@ -412,7 +412,7 @@ end it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it 'returns the query with "starred" property set to false' do @@ -453,7 +453,7 @@ context "unstarring his own query" do it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it 'returns the query with "starred" property set to true' do @@ -490,7 +490,7 @@ end it "succeeds" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "returns the form" do diff --git a/spec/requests/api/v3/queries/update_form_api_spec.rb b/spec/requests/api/v3/queries/update_form_api_spec.rb index 6d8fb54c8162..f4d3b1516906 100644 --- a/spec/requests/api/v3/queries/update_form_api_spec.rb +++ b/spec/requests/api/v3/queries/update_form_api_spec.rb @@ -65,7 +65,7 @@ end it "returns 200(OK)" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "is of type form" do diff --git a/spec/requests/api/v3/queries/update_query_spec.rb b/spec/requests/api/v3/queries/update_query_spec.rb index 50a9046d26c5..556549845d3a 100644 --- a/spec/requests/api/v3/queries/update_query_spec.rb +++ b/spec/requests/api/v3/queries/update_query_spec.rb @@ -121,7 +121,7 @@ def json end it "returns 200 (ok)" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "renders the updated query" do @@ -159,7 +159,7 @@ def json context "without EE", with_ee: false do it "yields a 422 error given a timestamp older than 1 day" do - expect(last_response.status).to eq 422 + expect(last_response).to have_http_status :unprocessable_entity expect(json["message"]).to eq "Timestamps contain forbidden values: #{timestamps.first}" end @@ -167,7 +167,7 @@ def json let(:timestamps) { ["oneDayAgo@12:00+00:00"] } it "returns 200 (ok)" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "updates the query timestamps" do @@ -188,7 +188,7 @@ def post! post! - expect(last_response.status).to eq 422 + expect(last_response).to have_http_status :unprocessable_entity expect(json["message"]).to eq "Project not found" end @@ -197,7 +197,7 @@ def post! post! - expect(last_response.status).to eq 422 + expect(last_response).to have_http_status :unprocessable_entity expect(json["message"]).to eq "Status Operator is not set to one of the allowed values." end @@ -206,7 +206,7 @@ def post! post! - expect(last_response.status).to eq 422 + expect(last_response).to have_http_status :unprocessable_entity expect(json["message"]).to eq "Statuz filter does not exist." end end diff --git a/spec/requests/api/v3/relations/relations_api_spec.rb b/spec/requests/api/v3/relations/relations_api_spec.rb index a64fbbb3a4db..3fa1effcc1de 100644 --- a/spec/requests/api/v3/relations/relations_api_spec.rb +++ b/spec/requests/api/v3/relations/relations_api_spec.rb @@ -93,7 +93,7 @@ end it "returns 201 (created)" do - expect(last_response.status).to eq(201) + expect(last_response).to have_http_status(:created) end it "has created a new relation" do @@ -121,7 +121,7 @@ end it "responds with error" do - expect(last_response.status).to be 422 + expect(last_response).to have_http_status :unprocessable_entity end it "states the reason for the error" do @@ -242,7 +242,7 @@ end it "returns 200 (ok)" do - expect(last_response.status).to eq 200 + expect(last_response).to have_http_status :ok end it "updates the relation's description" do @@ -267,7 +267,7 @@ end it "returns 422" do - expect(last_response.status).to eq 422 + expect(last_response).to have_http_status :unprocessable_entity end it "indicates an error with the type attribute" do @@ -291,7 +291,7 @@ end it "returns 422" do - expect(last_response.status).to eq 422 + expect(last_response).to have_http_status :unprocessable_entity end it "indicates an error with the `from` attribute" do @@ -329,7 +329,7 @@ context "with the required permissions" do it "works" do - expect(last_response.status).to eq 201 + expect(last_response).to have_http_status :created end end @@ -337,7 +337,7 @@ let(:permissions) { [:view_work_packages] } it "is forbidden" do - expect(last_response.status).to eq 403 + expect(last_response).to have_http_status :forbidden end end @@ -349,7 +349,7 @@ let!(:to) { create(:work_package) } it "returns 422" do - expect(last_response.status).to eq 422 + expect(last_response).to have_http_status :unprocessable_entity end it "indicates an error with the `to` attribute" do @@ -392,7 +392,7 @@ end it "returns 204 and destroy the relation" do - expect(last_response.status).to eq 204 + expect(last_response).to have_http_status :no_content expect(Relation.exists?(relation.id)).to be_falsey end @@ -400,7 +400,7 @@ let(:permissions) { %i[view_work_packages] } it "returns 403" do - expect(last_response.status).to eq 403 + expect(last_response).to have_http_status :forbidden end it "leaves the relation" do @@ -458,7 +458,7 @@ end it "returns 200" do - expect(last_response.status).to be 200 + expect(last_response).to have_http_status :ok end it "returns the visible relation (and only the visible one) satisfying the filter" do @@ -502,7 +502,7 @@ context "for a relation with visible work packages" do it "returns 200" do - expect(last_response.status).to be 200 + expect(last_response).to have_http_status :ok end it "returns the relation" do @@ -534,7 +534,7 @@ end it "returns 404 NOT FOUND" do - expect(last_response.status).to be 404 + expect(last_response).to have_http_status :not_found end end end diff --git a/spec/requests/api/v3/repositories/revisions_resource_spec.rb b/spec/requests/api/v3/repositories/revisions_resource_spec.rb index e060d753a6c5..31623d87ccf7 100644 --- a/spec/requests/api/v3/repositories/revisions_resource_spec.rb +++ b/spec/requests/api/v3/repositories/revisions_resource_spec.rb @@ -66,7 +66,7 @@ end it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end describe "response body" do diff --git a/spec/requests/api/v3/root_resource_spec.rb b/spec/requests/api/v3/root_resource_spec.rb index d7f6e3ed9aa1..46501b452b6b 100644 --- a/spec/requests/api/v3/root_resource_spec.rb +++ b/spec/requests/api/v3/root_resource_spec.rb @@ -59,7 +59,7 @@ context "when not login_required", with_settings: { login_required: false } do it "responds with 200", :aggregate_failures do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) expect(subject).to have_json_path("instanceName") end end @@ -73,7 +73,7 @@ end it "responds with 200" do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) end it "responds with a root representer" do @@ -82,7 +82,7 @@ context "without the X-requested-with header", :skip_xhr_header do it "returns OK because GET requests are allowed" do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) expect(subject).to have_json_path("instanceName") end end diff --git a/spec/requests/api/v3/status_resource_spec.rb b/spec/requests/api/v3/status_resource_spec.rb index b45371050d17..f9c83ad19418 100644 --- a/spec/requests/api/v3/status_resource_spec.rb +++ b/spec/requests/api/v3/status_resource_spec.rb @@ -82,16 +82,13 @@ end context "valid status id" do - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } end context "invalid status id" do let(:get_path) { api_v3_paths.status "bogus" } - it_behaves_like "param validation error" do - let(:id) { "bogus" } - let(:type) { "Status" } - end + it_behaves_like "not found" end end diff --git a/spec/requests/api/v3/support/api_helper.rb b/spec/requests/api/v3/support/api_helper.rb index a942810ec8b4..46fe6f6f33fe 100644 --- a/spec/requests/api/v3/support/api_helper.rb +++ b/spec/requests/api/v3/support/api_helper.rb @@ -27,7 +27,7 @@ #++ RSpec.shared_examples_for "safeguarded API" do - it { expect(last_response.status).to eq(404) } + it { expect(last_response).to have_http_status(:not_found) } end RSpec.shared_examples_for "valid activity request" do @@ -38,7 +38,7 @@ allow(User).to receive(:current).and_return(admin) end - it { expect(last_response.status).to eq(status_code) } + it { expect(last_response).to have_http_status(status_code) } describe "response body" do subject { last_response.body } @@ -56,5 +56,5 @@ allow(User).to receive(:current).and_return(admin) end - it { expect(last_response.status).to eq(422) } + it { expect(last_response).to have_http_status(:unprocessable_entity) } end diff --git a/spec/requests/api/v3/support/api_v3_collection_response.rb b/spec/requests/api/v3/support/api_v3_collection_response.rb index a3bde3e9c36a..132f949048b5 100644 --- a/spec/requests/api/v3/support/api_v3_collection_response.rb +++ b/spec/requests/api/v3/support/api_v3_collection_response.rb @@ -59,7 +59,7 @@ it "returns a collection successfully" do aggregate_failures do - expect(last_response.status).to eq(expected_status_code) + expect(last_response).to have_http_status(expected_status_code) errors = JSON.parse(subject).dig("_embedded", "errors")&.map { _1["message"] } expect(errors).to eq([]) if errors # make errors visible in console if any end diff --git a/spec/requests/api/v3/support/response_examples.rb b/spec/requests/api/v3/support/response_examples.rb index 2974b634ec78..3302c01a6cac 100644 --- a/spec/requests/api/v3/support/response_examples.rb +++ b/spec/requests/api/v3/support/response_examples.rb @@ -30,7 +30,7 @@ RSpec.shared_examples_for "successful response" do |code = 200| it "has the status code #{code}" do - expect(last_response.status).to eq(code) + expect(last_response).to have_http_status(code) end it "has a HAL+JSON Content-Type" do @@ -42,7 +42,7 @@ RSpec.shared_examples_for "successful no content response" do |code = 204| it "has the status code #{code}" do - expect(last_response.status).to eq(code) + expect(last_response).to have_http_status(code) end end @@ -50,7 +50,7 @@ let(:location) { "" } it "has the status code #{code}" do - expect(last_response.status).to eq(code) + expect(last_response).to have_http_status(code) end it "redirects to expected location" do @@ -64,7 +64,7 @@ end it "has the status code #{code}" do - expect(last_response.status).to eq(code) + expect(last_response).to have_http_status(code) end it "has a HAL+JSON Content-Type" do @@ -182,7 +182,7 @@ subject { JSON.parse(last_response.body) } it "results in a validation error" do - expect(last_response.status).to eq(400) + expect(last_response).to have_http_status(:bad_request) expect(subject["errorIdentifier"]).to eq("urn:openproject-org:api:v3:errors:BadRequest") expect(subject["message"]).to match /Bad request: .+? is invalid/ end diff --git a/spec/requests/api/v3/types/type_resource_spec.rb b/spec/requests/api/v3/types/type_resource_spec.rb index 0a0da5dff3fd..2b6934a37eb0 100644 --- a/spec/requests/api/v3/types/type_resource_spec.rb +++ b/spec/requests/api/v3/types/type_resource_spec.rb @@ -82,16 +82,13 @@ end context "valid type id" do - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } end context "invalid type id" do let(:get_path) { api_v3_paths.type "bogus" } - it_behaves_like "param validation error" do - let(:id) { "bogus" } - let(:type) { "Type" } - end + it_behaves_like "not found" end end diff --git a/spec/requests/api/v3/user/create_form_resource_spec.rb b/spec/requests/api/v3/user/create_form_resource_spec.rb index a9cd83532176..6bad65fa8981 100644 --- a/spec/requests/api/v3/user/create_form_resource_spec.rb +++ b/spec/requests/api/v3/user/create_form_resource_spec.rb @@ -55,7 +55,7 @@ it "returns a payload with validation errors", :aggregate_failures, with_settings: { default_language: :es } do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) expect(response.body).to be_json_eql("Form".to_json).at_path("_type") expect(body) @@ -101,7 +101,7 @@ end it "returns a valid payload", :aggregate_failures do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) expect(response.body).to be_json_eql("Form".to_json).at_path("_type") expect(body) @@ -149,7 +149,7 @@ end it "returns a valid form response" do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) expect(response.body).to be_json_eql("Form".to_json).at_path("_type") expect(body) diff --git a/spec/requests/api/v3/user/create_user_common_examples.rb b/spec/requests/api/v3/user/create_user_common_examples.rb index b55219a3786a..1026f3429f72 100644 --- a/spec/requests/api/v3/user/create_user_common_examples.rb +++ b/spec/requests/api/v3/user/create_user_common_examples.rb @@ -29,7 +29,7 @@ it "returns the represented user" do send_request - expect(last_response.status).to eq(201) + expect(last_response).to have_http_status(:created) expect(last_response.body).to have_json_type(Object).at_path("_links") expect(last_response.body) .to be_json_eql("User".to_json) @@ -50,7 +50,7 @@ attr = JSON.parse(last_response.body).dig "_embedded", "details", "attribute" - expect(last_response.status).to eq 422 + expect(last_response).to have_http_status :unprocessable_entity expect(attr).to eq attribute_name end end @@ -64,7 +64,7 @@ it "returns an erroneous response" do send_request - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) expect(errors.count).to eq(5) expect(errors.collect { |el| el["_embedded"]["details"]["attribute"] }) @@ -140,7 +140,7 @@ it "returns an erroneous response" do send_request - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) expect(errors).not_to be_empty expect(last_response.body) diff --git a/spec/requests/api/v3/user/create_user_resource_spec.rb b/spec/requests/api/v3/user/create_user_resource_spec.rb index 61ca8db92182..b873d767933d 100644 --- a/spec/requests/api/v3/user/create_user_resource_spec.rb +++ b/spec/requests/api/v3/user/create_user_resource_spec.rb @@ -114,7 +114,7 @@ def send_request it "returns an error on that attribute" do send_request - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) expect(last_response.body) .to be_json_eql("authSource".to_json) @@ -247,7 +247,7 @@ def send_request it "returns an erroneous response" do send_request - expect(last_response.status).to eq(403) + expect(last_response).to have_http_status(:forbidden) end end end diff --git a/spec/requests/api/v3/user/update_form_resource_spec.rb b/spec/requests/api/v3/user/update_form_resource_spec.rb index 94f5a5a5dff4..53505aaffe7a 100644 --- a/spec/requests/api/v3/user/update_form_resource_spec.rb +++ b/spec/requests/api/v3/user/update_form_resource_spec.rb @@ -67,7 +67,7 @@ describe "empty payload" do it "returns a valid form", :aggregate_failures do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) expect(response.body).to be_json_eql("Form".to_json).at_path("_type") expect(body) @@ -96,7 +96,7 @@ end it "returns a valid response", :aggregate_failures do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) expect(response.body).to be_json_eql("Form".to_json).at_path("_type") expect(subject.body) @@ -121,7 +121,7 @@ end it "returns an invalid form", :aggregate_failures do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) expect(response.body).to be_json_eql("Form".to_json).at_path("_type") expect(body) @@ -156,7 +156,7 @@ let(:path) { api_v3_paths.user_form(12345) } it "returns 404 Not found" do - expect(response.status).to eq(404) + expect(response).to have_http_status(:not_found) end end end diff --git a/spec/requests/api/v3/user/update_user_resource_spec.rb b/spec/requests/api/v3/user/update_user_resource_spec.rb index 272c1185ddd3..9279dc539f92 100644 --- a/spec/requests/api/v3/user/update_user_resource_spec.rb +++ b/spec/requests/api/v3/user/update_user_resource_spec.rb @@ -48,7 +48,7 @@ def send_request it "responds with the represented updated user" do send_request - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) expect(last_response.body).to have_json_type(Object).at_path("_links") expect(last_response.body) .to be_json_eql("User".to_json) @@ -83,7 +83,7 @@ def send_request it "returns an erroneous response" do send_request - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) expect(last_response.body) .to be_json_eql("email".to_json) @@ -107,7 +107,7 @@ def send_request it "updates the users password correctly" do send_request - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) updated_user = User.find(user.id) matches = updated_user.check_password?(password) @@ -121,7 +121,7 @@ def send_request it "responds with 404" do send_request - expect(last_response.status).to be(404) + expect(last_response).to have_http_status(:not_found) end end end @@ -138,7 +138,7 @@ def send_request it "rejects the users password update" do send_request - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) expect(last_response.body) .to be_json_eql("password".to_json) @@ -157,7 +157,7 @@ def send_request it "returns an erroneous response" do send_request - expect(last_response.status).to eq(403) + expect(last_response).to have_http_status(:forbidden) end end end diff --git a/spec/requests/api/v3/version_resource_spec.rb b/spec/requests/api/v3/version_resource_spec.rb index 58bd7dd67f97..fd6881033b12 100644 --- a/spec/requests/api/v3/version_resource_spec.rb +++ b/spec/requests/api/v3/version_resource_spec.rb @@ -55,7 +55,7 @@ shared_examples_for "successful response" do it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "returns the version" do @@ -157,7 +157,7 @@ end it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "updates the version" do @@ -297,7 +297,7 @@ end it "responds with 201" do - expect(last_response.status).to eq(201) + expect(last_response).to have_http_status(:created) end it "creates the version" do diff --git a/spec/requests/api/v3/versions/create_form_resource_spec.rb b/spec/requests/api/v3/versions/create_form_resource_spec.rb index 47fca5b70762..b91997919d98 100644 --- a/spec/requests/api/v3/versions/create_form_resource_spec.rb +++ b/spec/requests/api/v3/versions/create_form_resource_spec.rb @@ -50,7 +50,7 @@ describe "#POST /api/v3/versions/form" do it "returns 200 OK" do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) end it "returns a form" do @@ -186,7 +186,7 @@ let(:permissions) { [] } it "returns 403 Not Authorized" do - expect(response.status).to eq(403) + expect(response).to have_http_status(:forbidden) end end end diff --git a/spec/requests/api/v3/versions/update_form_resource_spec.rb b/spec/requests/api/v3/versions/update_form_resource_spec.rb index fbc564db2822..12ab7a95e4dc 100644 --- a/spec/requests/api/v3/versions/update_form_resource_spec.rb +++ b/spec/requests/api/v3/versions/update_form_resource_spec.rb @@ -55,7 +55,7 @@ describe "#POST /api/v3/versions/:id/form" do it "returns 200 OK" do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) end it "returns a form" do @@ -215,7 +215,7 @@ let(:permissions) { [:view_work_packages] } it "returns 403 Not Authorized" do - expect(response.status).to eq(403) + expect(response).to have_http_status(:forbidden) end end @@ -223,7 +223,7 @@ let(:permissions) { [] } it "returns 404 Not Found" do - expect(response.status).to eq(404) + expect(response).to have_http_status(:not_found) end end end diff --git a/spec/requests/api/v3/views/create_resource_spec.rb b/spec/requests/api/v3/views/create_resource_spec.rb index 8cdae129f461..5a9ebf045317 100644 --- a/spec/requests/api/v3/views/create_resource_spec.rb +++ b/spec/requests/api/v3/views/create_resource_spec.rb @@ -97,7 +97,7 @@ end it "responds with 422 and explains the error" do - expect(last_response.status).to eq(422) + expect(last_response).to have_http_status(:unprocessable_entity) expect(last_response.body) .to be_json_eql("Query does not exist.".to_json) diff --git a/spec/requests/api/v3/views/show_resource_spec.rb b/spec/requests/api/v3/views/show_resource_spec.rb index 606db351cf70..838982eab4c4 100644 --- a/spec/requests/api/v3/views/show_resource_spec.rb +++ b/spec/requests/api/v3/views/show_resource_spec.rb @@ -85,7 +85,7 @@ end it "returns a 404 response" do - expect(last_response.status).to eq(404) + expect(last_response).to have_http_status(:not_found) end end end diff --git a/spec/requests/api/v3/work_packages/by_project_create_resource_spec.rb b/spec/requests/api/v3/work_packages/by_project_create_resource_spec.rb index 7d4706a719b2..0caa698c112c 100644 --- a/spec/requests/api/v3/work_packages/by_project_create_resource_spec.rb +++ b/spec/requests/api/v3/work_packages/by_project_create_resource_spec.rb @@ -99,7 +99,7 @@ end it "returns Created(201)" do - expect(last_response.status).to eq(201) + expect(last_response).to have_http_status(:created) end it "creates a work package" do @@ -114,7 +114,7 @@ let(:current_user) { create(:user) } it "hides the endpoint" do - expect(last_response.status).to eq(404) + expect(last_response).to have_http_status(:not_found) end end @@ -124,7 +124,7 @@ let(:permissions) { [:view_project] } it "points out the missing permission" do - expect(last_response.status).to eq(403) + expect(last_response).to have_http_status(:forbidden) end end diff --git a/spec/requests/api/v3/work_packages/create_form_resource_spec.rb b/spec/requests/api/v3/work_packages/create_form_resource_spec.rb index 030ae7fc5a30..3130bab2aa4e 100644 --- a/spec/requests/api/v3/work_packages/create_form_resource_spec.rb +++ b/spec/requests/api/v3/work_packages/create_form_resource_spec.rb @@ -48,7 +48,7 @@ subject(:response) { last_response } it "returns 200(OK)" do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) end it "is of type form" do diff --git a/spec/requests/api/v3/work_packages/create_project_form_resource_spec.rb b/spec/requests/api/v3/work_packages/create_project_form_resource_spec.rb index 3510112baef7..92a2fa5f841a 100644 --- a/spec/requests/api/v3/work_packages/create_project_form_resource_spec.rb +++ b/spec/requests/api/v3/work_packages/create_project_form_resource_spec.rb @@ -44,7 +44,7 @@ subject(:response) { last_response } it "returns 200(OK)" do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) end it "is of type form" do diff --git a/spec/requests/api/v3/work_packages/create_resource_spec.rb b/spec/requests/api/v3/work_packages/create_resource_spec.rb index 82026528d9c1..51c183f47bac 100644 --- a/spec/requests/api/v3/work_packages/create_resource_spec.rb +++ b/spec/requests/api/v3/work_packages/create_resource_spec.rb @@ -109,7 +109,7 @@ end it "returns Created(201)" do - expect(last_response.status).to eq(201) + expect(last_response).to have_http_status(:created) end it "creates a work package" do @@ -132,7 +132,7 @@ let(:current_user) { create(:user) } it "hides the endpoint" do - expect(last_response.status).to eq(403) + expect(last_response).to have_http_status(:forbidden) end end @@ -142,7 +142,7 @@ let(:permissions) { [:view_project] } it "points out the missing permission" do - expect(last_response.status).to eq(403) + expect(last_response).to have_http_status(:forbidden) end end diff --git a/spec/requests/api/v3/work_packages/form/work_package_form_resource_spec.rb b/spec/requests/api/v3/work_packages/form/work_package_form_resource_spec.rb index 9726818b86bf..16f042a978fb 100644 --- a/spec/requests/api/v3/work_packages/form/work_package_form_resource_spec.rb +++ b/spec/requests/api/v3/work_packages/form/work_package_form_resource_spec.rb @@ -102,7 +102,7 @@ shared_examples_for "valid payload" do subject { last_response.body } - it { expect(last_response.status).to eq(200) } + it { expect(last_response).to have_http_status(:ok) } it { is_expected.to have_json_path("_embedded/payload") } @@ -232,7 +232,7 @@ include_context "with post request" - it { expect(last_response.status).to eq(409) } + it { expect(last_response).to have_http_status(:conflict) } it_behaves_like "update conflict" end @@ -803,7 +803,7 @@ end it "responds with a valid body (Regression OP#37510)" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end end end @@ -825,7 +825,7 @@ subject { last_response.body } shared_examples_for "valid payload" do - it { expect(last_response.status).to eq(200) } + it { expect(last_response).to have_http_status(:ok) } it { is_expected.to have_json_path("_embedded/payload") } diff --git a/spec/requests/api/v3/work_packages/show_resource_spec.rb b/spec/requests/api/v3/work_packages/show_resource_spec.rb index 9fb934c6cb1f..ad4c9f94e6f5 100644 --- a/spec/requests/api/v3/work_packages/show_resource_spec.rb +++ b/spec/requests/api/v3/work_packages/show_resource_spec.rb @@ -69,7 +69,7 @@ end it "responds with 200" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end describe "response body" do @@ -248,7 +248,7 @@ context "with EE", with_ee: %i[baseline_comparison] do it "responds with 200" do - expect(subject && last_response.status).to eq(200) + expect(subject && last_response).to have_http_status(:ok) end it "has the current attributes as attributes" do @@ -502,13 +502,13 @@ context "without EE" do shared_examples "success" do it "responds with 200" do - expect(subject && last_response.status).to eq(200) + expect(subject && last_response).to have_http_status(:ok) end end shared_examples "error" do it "responds with 400" do - expect(subject && last_response.status).to eq(400) + expect(subject && last_response).to have_http_status(:bad_request) end it "has the invalid timestamps message" do diff --git a/spec/requests/api/v3/work_packages/update_resource_spec.rb b/spec/requests/api/v3/work_packages/update_resource_spec.rb index 084aeefc6d1a..9113592e44f9 100644 --- a/spec/requests/api/v3/work_packages/update_resource_spec.rb +++ b/spec/requests/api/v3/work_packages/update_resource_spec.rb @@ -159,7 +159,7 @@ include_context "patch request" - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it "responds with updated work package subject" do expect(subject.body).to be_json_eql("Updated subject".to_json).at_path("subject") @@ -172,7 +172,7 @@ include_context "patch request" - it { expect(response.status).to eq(422) } + it { expect(response).to have_http_status(:unprocessable_entity) } it "has a readonly error" do expect(response.body) @@ -200,7 +200,7 @@ include_context "patch request" - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it_behaves_like "description updated" end @@ -214,7 +214,7 @@ include_context "patch request" - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it_behaves_like "description updated" end @@ -226,7 +226,7 @@ include_context "patch request" - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it "updates the scheduling mode" do expect(subject.body).to be_json_eql(schedule_manually.to_json).at_path("scheduleManually") @@ -239,7 +239,7 @@ include_context "patch request" - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it "responds with updated start date" do expect(subject.body).to be_json_eql(date_string.to_json).at_path("startDate") @@ -254,7 +254,7 @@ include_context "patch request" - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it "responds with updated finish date" do expect(subject.body).to be_json_eql(date_string.to_json).at_path("dueDate") @@ -269,7 +269,7 @@ include_context "patch request" - it { expect(response.status).to eq(200) } # rubocop:disable RSpecRails/HaveHttpStatus + it { expect(response).to have_http_status(:ok) } it "responds with updated finish date" do expect(subject.body).to be_json_eql(duration.to_json).at_path("remainingTime") @@ -297,7 +297,7 @@ include_context "patch request" - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it "responds with updated work package status" do expect(subject.body).to be_json_eql(target_status.name.to_json) @@ -349,7 +349,7 @@ include_context "patch request" - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it "responds with updated work package type" do expect(subject.body).to be_json_eql(target_type.name.to_json) @@ -425,7 +425,7 @@ include_context "patch request" it "is successful" do - expect(response.status).to eq(200) + expect(response).to have_http_status(:ok) end it_behaves_like "lock version updated" @@ -496,7 +496,7 @@ include_context "patch request" - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it { expect(response.body).to be_json_eql(nil.to_json).at_path(href_path) } @@ -507,7 +507,7 @@ shared_examples_for "valid user assignment" do let(:title) { assigned_user.name.to_s.to_json } - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it { expect(response.body) @@ -614,7 +614,7 @@ context "valid" do include_context "patch request" - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it "responds with the work package assigned to the version" do expect(subject.body) @@ -630,7 +630,7 @@ include_context "patch request" - it { expect(response.status).to eq(422) } + it { expect(response).to have_http_status(:unprocessable_entity) } it "has a readonly error" do expect(response.body) @@ -651,7 +651,7 @@ context "valid" do include_context "patch request" - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it "responds with the work package assigned to the category" do expect(subject.body) @@ -674,7 +674,7 @@ context "valid" do include_context "patch request" - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it "responds with the work package assigned to the priority" do expect(subject.body) @@ -698,7 +698,7 @@ context "valid" do include_context "patch request" - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it "responds with the work package and its new budget" do expect(subject.body).to be_json_eql(target_budget.subject.to_json) @@ -742,7 +742,7 @@ context "valid" do include_context "patch request" - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status(:ok) } it "responds with the work package assigned to the new value" do expect(subject.body) diff --git a/spec/requests/api/v3/work_packages/work_packages_schemas_resource_spec.rb b/spec/requests/api/v3/work_packages/work_packages_schemas_resource_spec.rb index 1e7d94f58e82..2aea024f1a05 100644 --- a/spec/requests/api/v3/work_packages/work_packages_schemas_resource_spec.rb +++ b/spec/requests/api/v3/work_packages/work_packages_schemas_resource_spec.rb @@ -59,7 +59,7 @@ context "authorized" do context "valid" do it "returns HTTP 200" do - expect(last_response.status).to be(200) + expect(last_response).to have_http_status(:ok) end it "returns a collection of schemas" do @@ -79,7 +79,7 @@ let(:filter_values) { ["#{0}-#{type.id}"] } it "returns HTTP 200" do - expect(last_response.status).to be(200) + expect(last_response).to have_http_status(:ok) end it "returns an empty collection" do @@ -93,7 +93,7 @@ let(:filter_values) { ["#{project.id}-#{0}"] } it "returns HTTP 200" do - expect(last_response.status).to be(200) + expect(last_response).to have_http_status(:ok) end it "returns an empty collection" do @@ -107,7 +107,7 @@ let(:filter_values) { ["bogus"] } it "returns HTTP 400" do - expect(last_response.status).to be(400) + expect(last_response).to have_http_status(:bad_request) end it "returns an error" do @@ -122,7 +122,7 @@ let(:role) { create(:project_role, permissions: []) } it "returns HTTP 403" do - expect(last_response.status).to be(403) + expect(last_response).to have_http_status(:forbidden) end end end @@ -138,7 +138,7 @@ context "valid schema" do it "returns HTTP 200" do - expect(last_response.status).to be(200) + expect(last_response).to have_http_status(:ok) end it "sets a weak ETag" do @@ -195,7 +195,7 @@ context "valid schema" do it "returns HTTP 200" do - expect(last_response.status).to be(200) + expect(last_response).to have_http_status(:ok) end # Further fields are tested in the representer specs diff --git a/spec/requests/oauth_clients/callback_flow_spec.rb b/spec/requests/oauth_clients/callback_flow_spec.rb index b62d6ce55864..88e5028f5865 100644 --- a/spec/requests/oauth_clients/callback_flow_spec.rb +++ b/spec/requests/oauth_clients/callback_flow_spec.rb @@ -60,7 +60,7 @@ context "when user is not logged in" do it "requires login" do get uri.to_s - expect(last_response.status).to eq(401) + expect(last_response).to have_http_status(:unauthorized) end end @@ -79,24 +79,23 @@ set_cookie "oauth_state_asdf1234=#{state_cookie}" end - # rubocop:disable RSpecRails/HaveHttpStatus shared_examples "with errors and state param with cookie, not being admin" do it "redirects to URI referenced in the state param and held in a cookie" do - expect(response.status).to eq(302) + expect(response).to have_http_status(:found) expect(response.location).to eq redirect_uri end end shared_examples "with errors, being an admin" do it "redirects to admin settings for the storage" do - expect(response.status).to eq(302) + expect(response).to have_http_status(:found) expect(URI(response.location).path).to eq edit_admin_settings_storage_path(oauth_client.integration) end end shared_examples "fallback redirect" do it "redirects to home" do - expect(response.status).to eq(302) + expect(response).to have_http_status(:found) expect(URI(response.location).path).to eq API::V3::Utilities::PathHelper::ApiV3Path::root_path end end @@ -110,7 +109,7 @@ it "redirects to the URL that was referenced by the state param and held by a cookie" do expect(rack_oauth2_client).to have_received(:authorization_code=).with(code) - expect(response.status).to eq 302 + expect(response).to have_http_status :found expect(response.location).to eq redirect_uri expect(OAuthClientToken.count).to eq 1 expect(OAuthClientToken.last.access_token).to eq "xyzaccesstoken" @@ -185,6 +184,5 @@ it_behaves_like "fallback redirect" end - # rubocop:enable RSpecRails/HaveHttpStatus end end diff --git a/spec/requests/oauth_clients/ensure_connection_flow_spec.rb b/spec/requests/oauth_clients/ensure_connection_flow_spec.rb index eced9e187b6e..e2799dfb9df7 100644 --- a/spec/requests/oauth_clients/ensure_connection_flow_spec.rb +++ b/spec/requests/oauth_clients/ensure_connection_flow_spec.rb @@ -45,7 +45,7 @@ context "when user is not logged in" do it "requires login" do get oauth_clients_ensure_connection_url(oauth_client_id: oauth_client.client_id) - expect(last_response.status).to eq(401) + expect(last_response).to have_http_status(:unauthorized) end end @@ -54,7 +54,7 @@ it "responds with 400 when storage_id parameter is absent" do get oauth_clients_ensure_connection_url(oauth_client_id: oauth_client.client_id) - expect(last_response.status).to eq(400) + expect(last_response).to have_http_status(:bad_request) expect(last_response.body).to eq("Required parameter missing: storage_id") end @@ -77,7 +77,7 @@ get oauth_clients_ensure_connection_url(oauth_client_id: oauth_client.client_id, storage_id: storage.id) oauth_client = storage.oauth_client - expect(last_response.status).to eq(302) + expect(last_response).to have_http_status(:found) expect(last_response.location).to eq( "#{storage.host}/index.php/apps/oauth2/authorize?client_id=" \ "#{oauth_client.client_id}&redirect_uri=#{CGI.escape(Rails.application.root_url)}" \ @@ -98,7 +98,7 @@ destination_url: "#{root_url}123") oauth_client = storage.oauth_client - expect(last_response.status).to eq(302) + expect(last_response).to have_http_status(:found) expect(last_response.location).to eq( "#{storage.host}/index.php/apps/oauth2/authorize?client_id=" \ "#{oauth_client.client_id}&redirect_uri=#{CGI.escape(Rails.application.root_url)}" \ @@ -118,7 +118,7 @@ destination_url: "#{storage.host}/index.php") oauth_client = storage.oauth_client - expect(last_response.status).to eq(302) + expect(last_response).to have_http_status(:found) expect(last_response.location).to eq( "#{storage.host}/index.php/apps/oauth2/authorize?client_id=" \ "#{oauth_client.client_id}&redirect_uri=#{CGI.escape(Rails.application.root_url)}" \ @@ -153,7 +153,7 @@ it "redirects to root_url" do get oauth_clients_ensure_connection_url(oauth_client_id: oauth_client.client_id, storage_id: storage.id) - expect(last_response.status).to eq(302) + expect(last_response).to have_http_status(:found) expect(last_response.location).to eq("http://www.example.com/") expect(last_response.cookies.keys).to eq(["_open_project_session"]) end @@ -167,7 +167,7 @@ destination_url: "#{root_url}123") storage.oauth_client - expect(last_response.status).to eq(302) + expect(last_response).to have_http_status(:found) expect(last_response.location).to eq("http://www.example.com/123") expect(last_response.cookies.keys).to eq(["_open_project_session"]) end @@ -180,7 +180,7 @@ destination_url: "#{storage.host}/index.php") storage.oauth_client - expect(last_response.status).to eq(302) + expect(last_response).to have_http_status(:found) expect(last_response.location).to eq("http://www.example.com/") expect(last_response.cookies.keys).to eq(["_open_project_session"]) end diff --git a/spec/requests/openid_google_provider_callback_spec.rb b/spec/requests/openid_google_provider_callback_spec.rb index f43e346ef8b8..43906e46995d 100644 --- a/spec/requests/openid_google_provider_callback_spec.rb +++ b/spec/requests/openid_google_provider_callback_spec.rb @@ -96,7 +96,7 @@ } } do response = get uri.to_s - expect(response.status).to eq(302) + expect(response).to have_http_status(:found) expect(response.location).to eq("http://example.org/two_factor_authentication/request") end end diff --git a/spec/requests/rate_limiting/api_v3_rate_limiting_spec.rb b/spec/requests/rate_limiting/api_v3_rate_limiting_spec.rb index 118f59eb379b..9ce85ffb6b69 100644 --- a/spec/requests/rate_limiting/api_v3_rate_limiting_spec.rb +++ b/spec/requests/rate_limiting/api_v3_rate_limiting_spec.rb @@ -33,6 +33,7 @@ include Rack::Test::Methods include API::V3::Utilities::PathHelper + shared_let(:project) { create(:project) } current_user { create(:admin) } context "when enabled", with_config: { rate_limiting: { api_v3: true } } do @@ -48,13 +49,13 @@ nil, "CONTENT_TYPE" => "application/json" - expect(last_response.status).to eq 200 + expect(last_response).to have_http_status :ok end post "/api/v3/work_packages/form", nil, "CONTENT_TYPE" => "application/json" - expect(last_response.status).to eq 429 + expect(last_response).to have_http_status :too_many_requests end end @@ -68,7 +69,7 @@ nil, "CONTENT_TYPE" => "application/json" - expect(last_response.status).to eq 200 + expect(last_response).to have_http_status :ok end end end diff --git a/spec/routing/project_queries_routing_spec.rb b/spec/routing/project_queries_routing_spec.rb index 42fb68c6238b..76146a55dd4b 100644 --- a/spec/routing/project_queries_routing_spec.rb +++ b/spec/routing/project_queries_routing_spec.rb @@ -29,16 +29,16 @@ require "spec_helper" RSpec.describe "Project query routes" do - it "/projects/queries/new GET routes to projects/queries#new" do - expect(get("/projects/queries/new")).to route_to("projects/queries#new") + it "/project_queries/new GET routes to projects/queries#new" do + expect(get("/project_queries/new")).to route_to("projects/queries#new") end - it "/projects/queries POST routes to projects/queries#create" do - expect(post("/projects/queries")).to route_to("projects/queries#create") + it "/project_queries POST routes to projects/queries#create" do + expect(post("/project_queries")).to route_to("projects/queries#create") end - it "/projects/queries/:id DELETE routes to projects/queries#destroy" do - expect(delete("/projects/queries/42")).to route_to("projects/queries#destroy", - id: "42") + it "/project_queries/:id DELETE routes to projects/queries#destroy" do + expect(delete("/project_queries/42")).to route_to("projects/queries#destroy", + id: "42") end end diff --git a/spec/routing/work_package/shares_spec.rb b/spec/routing/work_package/shares_spec.rb index 61a7cd5d2f4f..d87be5f471c9 100644 --- a/spec/routing/work_package/shares_spec.rb +++ b/spec/routing/work_package/shares_spec.rb @@ -29,58 +29,58 @@ require "spec_helper" RSpec.describe "work package share routes" do - it "connects GET /work_packages/:wp_id/shares to work_packages/shares#index" do - expect(get("/work_packages/1/shares")).to route_to(controller: "work_packages/shares", + it "connects GET /work_packages/:wp_id/shares to shares#index" do + expect(get("/work_packages/1/shares")).to route_to(controller: "shares", action: "index", work_package_id: "1") end - it "connects POST /work_packages/:wp_id/shares to work_packages/shares#create" do - expect(post("/work_packages/1/shares")).to route_to(controller: "work_packages/shares", + it "connects POST /work_packages/:wp_id/shares to shares#create" do + expect(post("/work_packages/1/shares")).to route_to(controller: "shares", action: "create", work_package_id: "1") end - it "connects DELETE /work_packages/:wp_id/shares/:id to work_packages/shares#delete" do - expect(delete("/work_packages/1/shares/5")).to route_to(controller: "work_packages/shares", + it "connects DELETE /work_packages/:wp_id/shares/:id to shares#delete" do + expect(delete("/work_packages/1/shares/5")).to route_to(controller: "shares", action: "destroy", id: "5", work_package_id: "1") end - it "connects PATCH /work_packages/:wp_id/shares/:id to work_packages/shares#update" do - expect(patch("/work_packages/1/shares/5")).to route_to(controller: "work_packages/shares", + it "connects PATCH /work_packages/:wp_id/shares/:id to shares#update" do + expect(patch("/work_packages/1/shares/5")).to route_to(controller: "shares", action: "update", id: "5", work_package_id: "1") end - it "connects PUT /work_packages/:wp_id/shares/:id to work_packages/shares#update" do - expect(put("/work_packages/1/shares/5")).to route_to(controller: "work_packages/shares", + it "connects PUT /work_packages/:wp_id/shares/:id to shares#update" do + expect(put("/work_packages/1/shares/5")).to route_to(controller: "shares", action: "update", id: "5", work_package_id: "1") end - it "connects POST /work_packages/:wp_id/shares/:id/resend_invite to work_packages/shares#resend_invite" do - expect(post("/work_packages/1/shares/5/resend_invite")).to route_to(controller: "work_packages/shares", + it "connects POST /work_packages/:wp_id/shares/:id/resend_invite to shares#resend_invite" do + expect(post("/work_packages/1/shares/5/resend_invite")).to route_to(controller: "shares", action: "resend_invite", id: "5", work_package_id: "1") end context "on bulk actions" do - it "routes DELETE /work_packages/:work_package_id/shares/bulk to work_packages/shares/bulk#destroy" do + it "routes DELETE /work_packages/:work_package_id/shares/bulk to shares/bulk#destroy" do expect(delete("/work_packages/1/shares/bulk")) - .to route_to(controller: "work_packages/shares/bulk", - action: "destroy", + .to route_to(controller: "shares", + action: "bulk_destroy", work_package_id: "1") end - it "routes PATCH /work_packages/:work_package_id/shares/bulk to work_packages/shares/bulk#update" do + it "routes PATCH /work_packages/:work_package_id/shares/bulk to shares/bulk#update" do expect(patch("/work_packages/1/shares/bulk")) - .to route_to(controller: "work_packages/shares/bulk", - action: "update", + .to route_to(controller: "shares", + action: "bulk_update", work_package_id: "1") end end diff --git a/spec/routing/work_packages_spec.rb b/spec/routing/work_packages_spec.rb index d80f95e60b2b..2b0b070ac2af 100644 --- a/spec/routing/work_packages_spec.rb +++ b/spec/routing/work_packages_spec.rb @@ -92,13 +92,13 @@ end it "connects GET /work_packages/:id/share to work_packages/shares#index" do - expect(get("/work_packages/1/shares")).to route_to(controller: "work_packages/shares", + expect(get("/work_packages/1/shares")).to route_to(controller: "shares", action: "index", work_package_id: "1") end it "connects POST /work_packages/:id/share to work_packages/shares#create" do - expect(post("/work_packages/1/shares")).to route_to(controller: "work_packages/shares", + expect(post("/work_packages/1/shares")).to route_to(controller: "shares", action: "create", work_package_id: "1") end diff --git a/spec/services/authorization/user_permissible_service_spec.rb b/spec/services/authorization/user_permissible_service_spec.rb index 5ca25900434c..75b22c978e48 100644 --- a/spec/services/authorization/user_permissible_service_spec.rb +++ b/spec/services/authorization/user_permissible_service_spec.rb @@ -2,6 +2,7 @@ RSpec.describe Authorization::UserPermissibleService do shared_let(:user) { create(:user) } + shared_let(:admin) { create(:admin) } shared_let(:anonymous_user) { create(:anonymous) } shared_let(:project) { create(:project) } shared_let(:work_package) { create(:work_package, project:) } @@ -161,6 +162,27 @@ it { is_expected.to be_allowed_in_project(permission, project) } end end + + context "and the user is admin" do + let(:queried_user) { admin } + + it { is_expected.to be_allowed_in_project(permission, project) } + + context "and the account is locked" do + before { admin.locked! } + + it { is_expected.not_to be_allowed_in_project(permission, project) } + end + + context "and the module the permission belongs to is disabled" do + before do + project.enabled_module_names = project.enabled_module_names - ["work_package_tracking"] + project.reload + end + + it { is_expected.not_to be_allowed_in_project(permission, project) } + end + end end context "and the user is a member of a project" do @@ -226,6 +248,27 @@ it { is_expected.not_to be_allowed_in_any_project(permission) } end end + + context "and the user is admin" do + let(:queried_user) { admin } + + it { is_expected.to be_allowed_in_any_project(permission) } + + context "and the account is locked" do + before { admin.locked! } + + it { is_expected.not_to be_allowed_in_any_project(permission) } + end + + context "and the module the permission belongs to is disabled" do + before do + project.enabled_module_names = project.enabled_module_names - ["work_package_tracking"] + project.reload + end + + it { is_expected.not_to be_allowed_in_any_project(permission) } + end + end end context "and the user is a member of a project" do @@ -292,6 +335,27 @@ it { is_expected.not_to be_allowed_in_entity(permission, work_package, WorkPackage) } end + context "and the user is admin" do + let(:queried_user) { admin } + + it { is_expected.to be_allowed_in_entity(permission, work_package, WorkPackage) } + + context "and the account is locked" do + before { admin.locked! } + + it { is_expected.not_to be_allowed_in_entity(permission, work_package, WorkPackage) } + end + + context "and the module the permission belongs to is disabled" do + before do + project.enabled_module_names = project.enabled_module_names - ["work_package_tracking"] + project.reload + end + + it { is_expected.not_to be_allowed_in_entity(permission, work_package, WorkPackage) } + end + end + context "and the user is a member of the project" do let(:role) { create(:project_role, permissions: [permission]) } let!(:project_member) { create(:member, user:, project:, roles: [role]) } @@ -305,7 +369,10 @@ end context "without the module enabled in the project" do - before { project.enabled_module_names = project.enabled_modules - [:work_package_tracking] } + before do + project.enabled_module_names = project.enabled_module_names - ["work_package_tracking"] + project.reload + end it { is_expected.not_to be_allowed_in_entity(permission, work_package, WorkPackage) } end @@ -369,6 +436,30 @@ it { is_expected.not_to be_allowed_in_any_entity(permission, WorkPackage) } end + context "and the user is admin" do + let(:queried_user) { admin } + + it { is_expected.to be_allowed_in_any_entity(permission, WorkPackage) } + it { is_expected.to be_allowed_in_any_entity(permission, WorkPackage, in_project: project) } + + context "and the account is locked" do + before { admin.locked! } + + it { is_expected.not_to be_allowed_in_any_entity(permission, WorkPackage) } + it { is_expected.not_to be_allowed_in_any_entity(permission, WorkPackage, in_project: project) } + end + + context "and the module the permission belongs to is disabled" do + before do + project.enabled_module_names = project.enabled_module_names - ["work_package_tracking"] + project.reload + end + + it { is_expected.not_to be_allowed_in_any_entity(permission, WorkPackage) } + it { is_expected.not_to be_allowed_in_any_entity(permission, WorkPackage, in_project: project) } + end + end + context "and the user is a member of a project" do let(:role) { create(:project_role, permissions: [permission]) } let!(:project_member) { create(:member, user:, project:, roles: [role]) } diff --git a/spec/services/members/delete_by_principal_service_spec.rb b/spec/services/members/delete_by_principal_service_spec.rb index 694fddd17bbf..af3e02d8ef75 100644 --- a/spec/services/members/delete_by_principal_service_spec.rb +++ b/spec/services/members/delete_by_principal_service_spec.rb @@ -99,19 +99,19 @@ member_roles: [build(:member_role), build(:member_role, inherited_from: 123)], entity: work_package_c) end - let(:service_instance_a) { instance_double(WorkPackageMembers::DeleteService, call: service_result_a) } - let(:service_instance_c) { instance_double(WorkPackageMembers::DeleteService, call: service_result_c) } + let(:service_instance_a) { instance_double(Shares::DeleteService, call: service_result_a) } + let(:service_instance_c) { instance_double(Shares::DeleteService, call: service_result_c) } let(:service_result_a) { ServiceResult.success } let(:service_result_c) { ServiceResult.success } before do - allow(WorkPackageMembers::DeleteService) + allow(Shares::DeleteService) .to receive(:new) - .with(user:, model: member_a) + .with(user:, model: member_a, contract_class: Shares::WorkPackages::DeleteContract) .and_return(service_instance_a) - allow(WorkPackageMembers::DeleteService) + allow(Shares::DeleteService) .to receive(:new) - .with(user:, model: member_c) + .with(user:, model: member_c, contract_class: Shares::WorkPackages::DeleteContract) .and_return(service_instance_c) end @@ -119,9 +119,9 @@ service_call expect(service_instance_a).to have_received(:call).with(no_args) - expect(WorkPackageMembers::DeleteService) + expect(Shares::DeleteService) .not_to have_received(:new) - .with(user:, model: member_b) + .with(user:, model: member_b, contract_class: Shares::WorkPackages::DeleteContract) expect(service_instance_c).to have_received(:call).with(no_args) end @@ -171,19 +171,19 @@ member_roles: [build(:member_role)], entity: work_package_d) end - let(:service_instance_a) { instance_double(WorkPackageMembers::DeleteRoleService, call: service_result_a) } - let(:service_instance_c) { instance_double(WorkPackageMembers::DeleteRoleService, call: service_result_c) } + let(:service_instance_a) { instance_double(Shares::DeleteRoleService, call: service_result_a) } + let(:service_instance_c) { instance_double(Shares::DeleteRoleService, call: service_result_c) } let(:service_result_a) { ServiceResult.success } let(:service_result_c) { ServiceResult.success } before do - allow(WorkPackageMembers::DeleteRoleService) + allow(Shares::DeleteRoleService) .to receive(:new) - .with(user:, model: member_a) + .with(user:, model: member_a, contract_class: Shares::WorkPackages::DeleteContract) .and_return(service_instance_a) - allow(WorkPackageMembers::DeleteRoleService) + allow(Shares::DeleteRoleService) .to receive(:new) - .with(user:, model: member_c) + .with(user:, model: member_c, contract_class: Shares::WorkPackages::DeleteContract) .and_return(service_instance_c) end @@ -191,13 +191,13 @@ service_call expect(service_instance_a).to have_received(:call).with(role_id: role.id.to_s) - expect(WorkPackageMembers::DeleteRoleService) + expect(Shares::DeleteRoleService) .not_to have_received(:new) - .with(user:, model: member_b) + .with(user:, model: member_b, contract_class: Shares::WorkPackages::DeleteContract) expect(service_instance_c).to have_received(:call).with(role_id: role.id.to_s) - expect(WorkPackageMembers::DeleteRoleService) + expect(Shares::DeleteRoleService) .not_to have_received(:new) - .with(user:, model: member_d) + .with(user:, model: member_d, contract_class: Shares::WorkPackages::DeleteContract) end context "when all calls succeed" do diff --git a/spec/services/news/create_service_spec.rb b/spec/services/news/create_service_spec.rb new file mode 100644 index 000000000000..df91f6f9d781 --- /dev/null +++ b/spec/services/news/create_service_spec.rb @@ -0,0 +1,34 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require "services/base_services/behaves_like_create_service" + +RSpec.describe News::CreateService, type: :model do + it_behaves_like "BaseServices create service" +end diff --git a/spec/services/news/delete_service_spec.rb b/spec/services/news/delete_service_spec.rb new file mode 100644 index 000000000000..dc0a20399c0f --- /dev/null +++ b/spec/services/news/delete_service_spec.rb @@ -0,0 +1,66 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" + +RSpec.describe News::DeleteService, type: :model do + let(:news) { build_stubbed(:news, project:) } + let(:project) { build_stubbed(:project) } + + let(:instance) { described_class.new(model: news, user: actor) } + + subject do + instance.call + end + + shared_examples "deletes the news" do + it do + expect(news).to receive(:destroy).and_return(true) + expect(subject).to be_success + end + end + + shared_examples "does not delete the news" do + it do + expect(news).not_to receive(:destroy) + expect(subject).not_to be_success + end + end + + context "with allowed user" do + let(:actor) { build_stubbed(:user) } + + before do + mock_permissions_for(actor) do |mock| + mock.allow_in_project(:manage_news, project:) + end + end + + it_behaves_like "deletes the news" + end +end diff --git a/spec/services/news/set_attributes_service_spec.rb b/spec/services/news/set_attributes_service_spec.rb new file mode 100644 index 000000000000..dea177d09995 --- /dev/null +++ b/spec/services/news/set_attributes_service_spec.rb @@ -0,0 +1,115 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" + +RSpec.describe News::SetAttributesService, type: :model do + let(:current_user) { build_stubbed(:user) } + + let(:contract_instance) do + contract = double("contract_instance") + allow(contract) + .to receive(:validate) + .and_return(contract_valid) + allow(contract) + .to receive(:errors) + .and_return(contract_errors) + contract + end + + let(:contract_errors) { double("contract_errors") } + let(:contract_valid) { true } + let(:model_valid) { true } + + let(:instance) do + described_class.new(user: current_user, + model: model_instance, + contract_class:, + contract_options: {}) + end + let(:model_instance) { News.new } + let(:contract_class) do + allow(News::CreateContract) + .to receive(:new) + .and_return(contract_instance) + + News::CreateContract + end + + let(:params) { {} } + + before do + allow(model_instance) + .to receive(:valid?) + .and_return(model_valid) + end + + subject { instance.call(params) } + + it "returns the instance as the result" do + expect(subject.result) + .to eql model_instance + end + + it "is a success" do + expect(subject) + .to be_success + end + + context "with params" do + let(:params) do + { + title: "Foobar" + } + end + + it "assigns the params" do + subject + + expect(model_instance.title).to eq "Foobar" + end + end + + context "with an invalid contract" do + let(:contract_valid) { false } + let(:expect_time_instance_save) do + expect(model_instance) + .not_to receive(:save) + end + + it "returns failure" do + expect(subject) + .not_to be_success + end + + it "returns the contract's errors" do + expect(subject.errors) + .to eql(contract_errors) + end + end +end diff --git a/spec/services/news/update_service_spec.rb b/spec/services/news/update_service_spec.rb new file mode 100644 index 000000000000..e1d2fbd813f9 --- /dev/null +++ b/spec/services/news/update_service_spec.rb @@ -0,0 +1,34 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require "services/base_services/behaves_like_update_service" + +RSpec.describe News::UpdateService, type: :model do + it_behaves_like "BaseServices update service" +end diff --git a/spec/services/params_to_query_service_project_query_spec.rb b/spec/services/params_to_query_service_project_query_spec.rb index 9fbf8351f625..0aa3154c03ca 100644 --- a/spec/services/params_to_query_service_project_query_spec.rb +++ b/spec/services/params_to_query_service_project_query_spec.rb @@ -46,7 +46,7 @@ it "returns a new query" do expect(service_call) - .to be_a Queries::Projects::ProjectQuery + .to be_a ProjectQuery end it "applies the filters" do @@ -59,7 +59,7 @@ context "when sending neither filters nor orders props" do it "returns a new query" do expect(service_call) - .to be_a Queries::Projects::ProjectQuery + .to be_a ProjectQuery end it "sets no name" do diff --git a/spec/services/projects/copy_service_integration_spec.rb b/spec/services/projects/copy_service_integration_spec.rb index 6e3e43935cff..513e95ec1d84 100644 --- a/spec/services/projects/copy_service_integration_spec.rb +++ b/spec/services/projects/copy_service_integration_spec.rb @@ -188,9 +188,9 @@ end end - context 'with disabled project custom fields with default value' do - it 'is still disabled in the copy' do - create(:text_project_custom_field, default_value: 'default value') + context "with disabled project custom fields with default value" do + it "is still disabled in the copy" do + create(:text_project_custom_field, default_value: "default value") expect(subject).to be_success diff --git a/spec/services/queries/projects/project_queries/create_service_spec.rb b/spec/services/queries/projects/project_queries/create_service_spec.rb index d33f19686d43..1ee49ee46f5e 100644 --- a/spec/services/queries/projects/project_queries/create_service_spec.rb +++ b/spec/services/queries/projects/project_queries/create_service_spec.rb @@ -31,6 +31,7 @@ RSpec.describe Queries::Projects::ProjectQueries::CreateService, type: :model do it_behaves_like "BaseServices create service" do + let(:model_class) { ProjectQuery } let(:factory) { :project_query } end diff --git a/spec/services/queries/projects/project_queries/set_attributes_service_spec.rb b/spec/services/queries/projects/project_queries/set_attributes_service_spec.rb index 06ae67e47d16..6165a87b5658 100644 --- a/spec/services/queries/projects/project_queries/set_attributes_service_spec.rb +++ b/spec/services/queries/projects/project_queries/set_attributes_service_spec.rb @@ -45,7 +45,7 @@ contract_class:, contract_options: {}) end - let(:model_instance) { Queries::Projects::ProjectQuery.new } + let(:model_instance) { ProjectQuery.new } let(:contract_class) do allow(Queries::Projects::ProjectQueries::CreateContract) .to receive(:new) @@ -191,7 +191,7 @@ context "with the query already having order and with order params" do let(:model_instance) do - Queries::Projects::ProjectQuery.new.tap do |query| + ProjectQuery.new.tap do |query| query.order(lft: :asc) end end @@ -224,7 +224,7 @@ context "with the query already having filters and with filter params" do let(:model_instance) do - Queries::Projects::ProjectQuery.new.tap do |query| + ProjectQuery.new.tap do |query| query.where("active", "=", ["t"]) end end @@ -254,7 +254,7 @@ context "with the query already having selects and with selects params" do let(:model_instance) do - Queries::Projects::ProjectQuery.new.tap do |query| + ProjectQuery.new.tap do |query| query.select(:id, :name) end end diff --git a/spec/services/work_package_members/create_or_update_service_spec.rb b/spec/services/shares/create_or_update_service_spec.rb similarity index 67% rename from spec/services/work_package_members/create_or_update_service_spec.rb rename to spec/services/shares/create_or_update_service_spec.rb index 5ea7609054d7..6793f81e57d3 100644 --- a/spec/services/work_package_members/create_or_update_service_spec.rb +++ b/spec/services/shares/create_or_update_service_spec.rb @@ -30,11 +30,13 @@ require "spec_helper" -RSpec.describe WorkPackageMembers::CreateOrUpdateService do +RSpec.describe Shares::CreateOrUpdateService do let(:user) { build_stubbed(:user) } let(:role) { build_stubbed(:view_work_package_role) } let(:work_package) { build_stubbed(:work_package) } - let(:instance) { described_class.new(user:) } + let(:create_contract_class) { class_double(Shares::WorkPackages::CreateContract) } + let(:update_contract_class) { class_double(Shares::WorkPackages::UpdateContract) } + let(:instance) { described_class.new(user:, create_contract_class:, update_contract_class:) } let(:params) { { user_id: user, roles: [role], entity: work_package } } let(:service_result) { instance_double(ServiceResult) } @@ -48,12 +50,16 @@ .and_return(existing_member) end - context "when the user has no work_package_member for that work package" do - let(:create_instance) { instance_double(WorkPackageMembers::CreateService) } + context "when the user is not a member of the shared entity" do + let(:create_instance) { instance_double(Shares::CreateService) } let(:existing_member) { nil } - it "calls the WorkPackageMembers::CreateService" do - allow(WorkPackageMembers::CreateService).to receive(:new).and_return(create_instance) + it "calls the Shares::CreateService" do + allow(Shares::CreateService).to receive(:new).with( + contract_class: create_contract_class, + contract_options: {}, + user: + ).and_return(create_instance) allow(create_instance).to receive(:call).and_return(service_result) service_call @@ -64,12 +70,17 @@ end end - context "when the user already has a work_package_member for that work package" do - let(:update_instance) { instance_double(WorkPackageMembers::UpdateService) } + context "when the user is already a member of the shared entity" do + let(:update_instance) { instance_double(Shares::UpdateService) } let(:existing_member) { build_stubbed(:work_package_member) } - it "calls the WorkPackageMembers::UpdateService" do - allow(WorkPackageMembers::UpdateService).to receive(:new).and_return(update_instance) + it "calls the Shares::UpdateService" do + allow(Shares::UpdateService).to receive(:new).with( + contract_class: update_contract_class, + contract_options: {}, + model: existing_member, + user: + ).and_return(update_instance) allow(update_instance).to receive(:call).and_return(service_result) service_call diff --git a/spec/services/work_package_members/create_service_spec.rb b/spec/services/shares/create_service_spec.rb similarity index 97% rename from spec/services/work_package_members/create_service_spec.rb rename to spec/services/shares/create_service_spec.rb index a137bafbf06c..058974522727 100644 --- a/spec/services/work_package_members/create_service_spec.rb +++ b/spec/services/shares/create_service_spec.rb @@ -31,7 +31,7 @@ require "spec_helper" require "services/base_services/behaves_like_create_service" -RSpec.describe WorkPackageMembers::CreateService, type: :model do +RSpec.describe Shares::CreateService, type: :model do subject(:service_call) { instance.call(call_attributes) } let(:instance) { described_class.new(user:) } @@ -70,6 +70,7 @@ def stub_notifications before { stub_notifications } it_behaves_like "BaseServices create service" do + let(:factory) { :work_package_member } let(:model_class) { Member } let(:principal) { richard } let(:call_attributes) { { principal:, roles: [role], entity: work_package, project: work_package.project } } diff --git a/spec/services/work_package_members/delete_role_service_spec.rb b/spec/services/shares/delete_role_service_spec.rb similarity index 97% rename from spec/services/work_package_members/delete_role_service_spec.rb rename to spec/services/shares/delete_role_service_spec.rb index 8625551c5dae..b5ebce34b51c 100644 --- a/spec/services/work_package_members/delete_role_service_spec.rb +++ b/spec/services/shares/delete_role_service_spec.rb @@ -31,8 +31,9 @@ require "spec_helper" require "services/base_services/behaves_like_delete_service" -RSpec.describe WorkPackageMembers::DeleteRoleService, type: :model do +RSpec.describe Shares::DeleteRoleService, type: :model do it_behaves_like "BaseServices delete service" do + let(:factory) { :work_package_member } let(:model_class) { Member } let(:model_instance) { build_stubbed(:work_package_member, principal:) } let(:principal) { user } diff --git a/spec/services/work_package_members/delete_service_spec.rb b/spec/services/shares/delete_service_spec.rb similarity index 97% rename from spec/services/work_package_members/delete_service_spec.rb rename to spec/services/shares/delete_service_spec.rb index ff30cc211b0a..0df47449fb8d 100644 --- a/spec/services/work_package_members/delete_service_spec.rb +++ b/spec/services/shares/delete_service_spec.rb @@ -31,8 +31,9 @@ require "spec_helper" require "services/base_services/behaves_like_delete_service" -RSpec.describe WorkPackageMembers::DeleteService, type: :model do +RSpec.describe Shares::DeleteService, type: :model do it_behaves_like "BaseServices delete service" do + let(:factory) { :work_package_member } let(:model_class) { Member } let(:model_instance) { build_stubbed(:work_package_member, principal:) } let(:principal) { user } diff --git a/spec/services/work_package_members/set_attributes_service_spec.rb b/spec/services/shares/set_attributes_service_spec.rb similarity index 96% rename from spec/services/work_package_members/set_attributes_service_spec.rb rename to spec/services/shares/set_attributes_service_spec.rb index b7111e67fe8d..874f97e5d2dd 100644 --- a/spec/services/work_package_members/set_attributes_service_spec.rb +++ b/spec/services/shares/set_attributes_service_spec.rb @@ -28,7 +28,7 @@ require "spec_helper" -RSpec.describe WorkPackageMembers::SetAttributesService, type: :model do +RSpec.describe Shares::SetAttributesService, type: :model do let(:user) { build_stubbed(:user) } let(:work_package) { build_stubbed(:work_package) } let(:member) do @@ -38,15 +38,15 @@ let(:existing_member) { build_stubbed(:work_package_member) } let(:contract_class) do - allow(WorkPackageMembers::CreateContract) + allow(Shares::WorkPackages::CreateContract) .to receive(:new) .with(member, user, options: {}) .and_return(contract_instance) - WorkPackageMembers::CreateContract + Shares::WorkPackages::CreateContract end let(:contract_instance) do - instance_double(WorkPackageMembers::CreateContract, validate: contract_valid, errors: contract_errors) + instance_double(Shares::WorkPackages::CreateContract, validate: contract_valid, errors: contract_errors) end let(:contract_valid) { true } let(:contract_errors) do diff --git a/spec/services/work_package_members/update_service_spec.rb b/spec/services/shares/update_service_spec.rb similarity index 98% rename from spec/services/work_package_members/update_service_spec.rb rename to spec/services/shares/update_service_spec.rb index d2ffd4fd663a..cff496da09ba 100644 --- a/spec/services/work_package_members/update_service_spec.rb +++ b/spec/services/shares/update_service_spec.rb @@ -31,7 +31,7 @@ require "spec_helper" require "services/base_services/behaves_like_update_service" -RSpec.describe WorkPackageMembers::UpdateService do +RSpec.describe Shares::UpdateService do let!(:groups_update_roles_service) do instance_double(Groups::UpdateRolesService).tap do |service_double| allow(Groups::UpdateRolesService) diff --git a/spec/services/users/drop_tokens_service_spec.rb b/spec/services/users/drop_tokens_service_spec.rb index 5a4c93c030ce..84b08f926284 100644 --- a/spec/services/users/drop_tokens_service_spec.rb +++ b/spec/services/users/drop_tokens_service_spec.rb @@ -33,6 +33,7 @@ shared_let(:other_user) { create(:user) } let(:instance) { described_class.new(current_user: input_user) } + subject { instance.call! } describe "Invitation token" do diff --git a/spec/services/work_packages/set_schedule_service_spec.rb b/spec/services/work_packages/set_schedule_service_spec.rb index cbae618927a9..65e0f35d1139 100644 --- a/spec/services/work_packages/set_schedule_service_spec.rb +++ b/spec/services/work_packages/set_schedule_service_spec.rb @@ -26,14 +26,14 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require 'spec_helper' +require "spec_helper" RSpec.describe WorkPackages::SetScheduleService do create_shared_association_defaults_for_work_package_factory let(:work_package) do create(:work_package, - subject: 'subject', + subject: "subject", start_date: work_package_start_date, due_date: work_package_due_date) end @@ -123,17 +123,17 @@ def create_child(parent, start_date, due_date) subject { instance.call(attributes) } - shared_examples_for 'reschedules' do + shared_examples_for "reschedules" do before do subject end - it 'is success' do + it "is success" do expect(subject) .to be_success end - it 'updates the following work packages' do + it "updates the following work packages" do expected.each do |wp, (start_date, due_date)| expected_cause_type = "work_package_related_changed_times" result = subject.all_results.find { |result_wp| result_wp.id == wp.id } @@ -168,87 +168,87 @@ def create_child(parent, start_date, due_date) end end - it 'returns only the original and the changed work packages' do + it "returns only the original and the changed work packages" do expect(subject.all_results) .to match_array expected.keys + [work_package] end end - shared_examples_for 'does not reschedule' do + shared_examples_for "does not reschedule" do before do subject end - it 'is success' do + it "is success" do expect(subject) .to be_success end - it 'does not change any other work packages' do + it "does not change any other work packages" do expect(subject.all_results) .to contain_exactly(work_package) end - it 'does not assign a journal cause' do + it "does not assign a journal cause" do subject.all_results.each do |work_package| expect(work_package.journal_cause).to be_blank end end end - context 'without relation' do - it 'is success' do + context "without relation" do + it "is success" do expect(subject) .to be_success end end - context 'with a single successor' do + context "with a single successor" do let!(:following) do [following_work_package1] end - context 'when moving forward' do + context "when moving forward" do before do work_package.due_date = Time.zone.today + 5.days end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days] } end end end - context 'when moving forward with the follower having no due date' do + context "when moving forward with the follower having no due date" do let(:follower1_due_date) { nil } before do work_package.due_date = Time.zone.today + 5.days end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [Time.zone.today + 6.days, nil] } end end end - context 'when moving forward with the follower having no start date' do + context "when moving forward with the follower having no start date" do let(:follower1_start_date) { nil } before do work_package.due_date = Time.zone.today + 5.days end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 6.days] } end end end - context 'when moving forward with the follower having some space left' do + context "when moving forward with the follower having some space left" do let(:follower1_start_date) { Time.zone.today + 3.days } let(:follower1_due_date) { Time.zone.today + 5.days } @@ -256,14 +256,14 @@ def create_child(parent, start_date, due_date) work_package.due_date = Time.zone.today + 5.days end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days] } end end end - context 'when moving forward with the follower having enough space left to not be moved at all' do + context "when moving forward with the follower having enough space left to not be moved at all" do let(:follower1_start_date) { Time.zone.today + 10.days } let(:follower1_due_date) { Time.zone.today + 12.days } @@ -271,10 +271,10 @@ def create_child(parent, start_date, due_date) work_package.due_date = Time.zone.today + 5.days end - it_behaves_like 'does not reschedule' + it_behaves_like "does not reschedule" end - context 'when moving forward with the follower having some space left and a lag' do + context "when moving forward with the follower having some space left and a lag" do let(:follower1_start_date) { Time.zone.today + 5.days } let(:follower1_due_date) { Time.zone.today + 7.days } let(:follower1_lag) { 3 } @@ -283,14 +283,14 @@ def create_child(parent, start_date, due_date) work_package.due_date = Time.zone.today + 5.days end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [Time.zone.today + 9.days, Time.zone.today + 11.days] } end end end - context 'when moving forward with the follower not needing to be moved' do + context "when moving forward with the follower not needing to be moved" do let(:follower1_start_date) { Time.zone.today + 6.days } let(:follower1_due_date) { Time.zone.today + 8.days } @@ -298,18 +298,18 @@ def create_child(parent, start_date, due_date) work_package.due_date = Time.zone.today + 5.days end - it_behaves_like 'does not reschedule' + it_behaves_like "does not reschedule" end - context 'when moving backwards' do + context "when moving backwards" do before do work_package.due_date = Time.zone.today - 5.days end - it_behaves_like 'does not reschedule' + it_behaves_like "does not reschedule" end - context 'when moving backwards with space between' do + context "when moving backwards with space between" do let(:follower1_start_date) { Time.zone.today + 3.days } let(:follower1_due_date) { Time.zone.today + 5.days } @@ -317,7 +317,7 @@ def create_child(parent, start_date, due_date) work_package.due_date = Time.zone.today - 5.days end - it_behaves_like 'does not reschedule' + it_behaves_like "does not reschedule" end context 'when moving backwards with the follower having no start date (which should not happen) \ @@ -328,7 +328,7 @@ def create_child(parent, start_date, due_date) work_package.due_date = Time.zone.today - 5.days end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [Time.zone.today - 4.days, follower1_due_date] } end @@ -343,82 +343,82 @@ def create_child(parent, start_date, due_date) work_package.due_date = follower1_due_date + 5.days end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [follower1_due_date + 6.days, follower1_due_date + 6.days] } end end end - context 'when removing the dates on the predecessor' do + context "when removing the dates on the predecessor" do before do work_package.start_date = work_package.due_date = nil end # The follower will keep its dates - it_behaves_like 'does not reschedule' + it_behaves_like "does not reschedule" - context 'when the follower has no start date but a due date' do + context "when the follower has no start date but a due date" do let(:follower1_start_date) { nil } let(:follower1_due_date) { Time.zone.today + 15.days } - it_behaves_like 'does not reschedule' + it_behaves_like "does not reschedule" end end - context 'when not moving and the successor not having start & due date (e.g. creating relation)' do + context "when not moving and the successor not having start & due date (e.g. creating relation)" do let(:follower1_start_date) { nil } let(:follower1_due_date) { nil } - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [work_package.due_date + 1.day, nil] } end end end - context 'when not moving and the successor having due before predecessor due date (e.g. creating relation)' do + context "when not moving and the successor having due before predecessor due date (e.g. creating relation)" do let(:follower1_start_date) { nil } let(:follower1_due_date) { work_package_due_date - 5.days } - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [work_package.due_date + 1.day, work_package.due_date + 1.day] } end end end - context 'when not moving and the successor having start before predecessor due date (e.g. creating relation)' do + context "when not moving and the successor having start before predecessor due date (e.g. creating relation)" do let(:follower1_start_date) { work_package_due_date - 5.days } let(:follower1_due_date) { nil } - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [work_package.due_date + 1.day, nil] } end end end - context 'when not moving and the successor having start and due before predecessor due date (e.g. creating relation)' do + context "when not moving and the successor having start and due before predecessor due date (e.g. creating relation)" do let(:follower1_start_date) { work_package_due_date - 5.days } let(:follower1_due_date) { work_package_due_date - 2.days } - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [work_package.due_date + 1.day, work_package.due_date + 4.days] } end end end - context 'when not having dates and the successor not having start & due date (e.g. creating relation)' do + context "when not having dates and the successor not having start & due date (e.g. creating relation)" do let(:work_package_due_date) { nil } let(:follower1_start_date) { nil } let(:follower1_due_date) { nil } - it_behaves_like 'does not reschedule' + it_behaves_like "does not reschedule" end - context 'with the successor having another predecessor which has no dates' do + context "with the successor having another predecessor which has no dates" do let(:following_work_package1) do create_follower(follower1_start_date, follower1_due_date, @@ -431,29 +431,29 @@ def create_child(parent, start_date, due_date) due_date: nil) end - context 'when moving forward' do + context "when moving forward" do before do work_package.due_date = Time.zone.today + 5.days end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days] } end end end - context 'when moving backwards' do + context "when moving backwards" do before do work_package.due_date = Time.zone.today - 5.days end - it_behaves_like 'does not reschedule' + it_behaves_like "does not reschedule" end end end - context 'with only a parent' do + context "with only a parent" do let!(:parent_work_package) do create(:work_package).tap do |parent| work_package.parent = parent @@ -462,14 +462,14 @@ def create_child(parent, start_date, due_date) end let(:work_package_start_date) { Time.zone.today - 5.days } - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { parent_work_package => [work_package_start_date, work_package_due_date] } end end end - context 'with a parent having a follower' do + context "with a parent having a follower" do let(:work_package_start_date) { Time.zone.today } let(:work_package_due_date) { Time.zone.today + 5.days } let!(:parent_work_package) do @@ -487,7 +487,7 @@ def create_child(parent, start_date, due_date) { parent_work_package => 0 }) end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { parent_work_package => [work_package_start_date, work_package_due_date], follower_of_parent_work_package => [work_package_due_date + 1.day, work_package_due_date + 3.days] } @@ -514,7 +514,7 @@ def create_child(parent, start_date, due_date) # # That's why the WorkPackage.for_scheduling call is mocked to customize # the order of the returned work_packages to reproduce this bug. - context 'with also a sibling follower with same parent' do + context "with also a sibling follower with same parent" do let!(:sibling_follower_of_work_package) do create_follower(Time.zone.today + 2.days, Time.zone.today + 3.days, @@ -531,7 +531,7 @@ def create_child(parent, start_date, due_date) end end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { sibling_follower_of_work_package => [work_package_due_date + 1.day, work_package_due_date + 2.days], parent_work_package => [work_package_start_date, work_package_due_date + 2.days], @@ -541,18 +541,18 @@ def create_child(parent, start_date, due_date) end end - context 'with a single successor having a parent' do + context "with a single successor having a parent" do let!(:following) do [following_work_package1, parent_following_work_package1] end - context 'when moving forward' do + context "when moving forward" do before do work_package.due_date = Time.zone.today + 5.days end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days], parent_following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days] } @@ -560,7 +560,7 @@ def create_child(parent, start_date, due_date) end end - context 'when moving forward with the parent having another child not being moved' do + context "when moving forward with the parent having another child not being moved" do let(:parent_follower1_start_date) { follower1_start_date } let(:parent_follower1_due_date) { follower1_due_date + 4.days } @@ -574,7 +574,7 @@ def create_child(parent, start_date, due_date) work_package.due_date = Time.zone.today + 5.days end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days], parent_following_work_package1 => [Time.zone.today + 5.days, Time.zone.today + 8.days] } @@ -582,16 +582,16 @@ def create_child(parent, start_date, due_date) end end - context 'when moving backwards' do + context "when moving backwards" do before do work_package.due_date = Time.zone.today - 5.days end - it_behaves_like 'does not reschedule' + it_behaves_like "does not reschedule" end end - context 'with a single successor having a child' do + context "with a single successor having a child" do let(:child_start_date) { follower1_start_date } let(:child_due_date) { follower1_due_date } @@ -602,12 +602,12 @@ def create_child(parent, start_date, due_date) child_work_package] end - context 'when moving forward' do + context "when moving forward" do before do work_package.due_date = Time.zone.today + 5.days end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days], child_work_package => [Time.zone.today + 6.days, Time.zone.today + 8.days] } @@ -616,7 +616,7 @@ def create_child(parent, start_date, due_date) end end - context 'with a single successor having two children' do + context "with a single successor having two children" do let(:follower1_start_date) { work_package_due_date + 1.day } let(:follower1_due_date) { work_package_due_date + 10.days } let(:child1_start_date) { follower1_start_date } @@ -633,11 +633,11 @@ def create_child(parent, start_date, due_date) child2_work_package] end - context 'with unchanged dates (e.g. when creating a follows relation) and successor starting 1 day after scheduled' do - it_behaves_like 'does not reschedule' + context "with unchanged dates (e.g. when creating a follows relation) and successor starting 1 day after scheduled" do + it_behaves_like "does not reschedule" end - context 'with unchanged dates (e.g. when creating a follows relation) and successor starting 3 days after scheduled' do + context "with unchanged dates (e.g. when creating a follows relation) and successor starting 3 days after scheduled" do let(:follower1_start_date) { work_package_due_date + 3.days } let(:follower1_due_date) { follower1_start_date + 10.days } let(:child1_start_date) { follower1_start_date } @@ -645,10 +645,10 @@ def create_child(parent, start_date, due_date) let(:child2_start_date) { follower1_start_date + 8.days } let(:child2_due_date) { follower1_due_date } - it_behaves_like 'does not reschedule' + it_behaves_like "does not reschedule" end - context 'with unchanged dates (e.g. when creating a follows relation) and successor\'s first child needs to be rescheduled' do + context "with unchanged dates (e.g. when creating a follows relation) and successor's first child needs to be rescheduled" do let(:follower1_start_date) { work_package_due_date - 3.days } let(:follower1_due_date) { work_package_due_date + 10.days } let(:child1_start_date) { follower1_start_date } @@ -657,7 +657,7 @@ def create_child(parent, start_date, due_date) let(:child2_due_date) { follower1_due_date } # following parent is reduced in length as the children allow to be executed at the same time - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [work_package_due_date + 1.day, follower1_due_date], child1_work_package => [work_package_due_date + 1.day, follower1_start_date + 10.days] } @@ -674,7 +674,7 @@ def create_child(parent, start_date, due_date) let(:child2_due_date) { follower1_due_date } # following parent is reduced in length and children are rescheduled - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [work_package_due_date + 1.day, follower1_start_date + 21.days], child1_work_package => [work_package_due_date + 1.day, child1_due_date + 9.days], @@ -684,7 +684,7 @@ def create_child(parent, start_date, due_date) end end - context 'with a chain of successors' do + context "with a chain of successors" do let(:follower1_start_date) { Time.zone.today + 1.day } let(:follower1_due_date) { Time.zone.today + 3.days } let(:follower2_start_date) { Time.zone.today + 4.days } @@ -698,12 +698,12 @@ def create_child(parent, start_date, due_date) following_work_package3] end - context 'when moving forward' do + context "when moving forward" do before do work_package.due_date = Time.zone.today + 5.days end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days], following_work_package2 => [Time.zone.today + 9.days, Time.zone.today + 13.days], @@ -712,7 +712,7 @@ def create_child(parent, start_date, due_date) end end - context 'when moving forward with some space between the followers' do + context "when moving forward with some space between the followers" do let(:follower1_start_date) { Time.zone.today + 1.day } let(:follower1_due_date) { Time.zone.today + 3.days } let(:follower2_start_date) { Time.zone.today + 7.days } @@ -724,7 +724,7 @@ def create_child(parent, start_date, due_date) work_package.due_date = Time.zone.today + 5.days end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days], following_work_package2 => [Time.zone.today + 9.days, Time.zone.today + 12.days] } @@ -732,16 +732,16 @@ def create_child(parent, start_date, due_date) end end - context 'when moving backwards' do + context "when moving backwards" do before do work_package.due_date = Time.zone.today - 5.days end - it_behaves_like 'does not reschedule' + it_behaves_like "does not reschedule" end end - context 'with a chain of successors with two paths leading to the same work package in the end' do + context "with a chain of successors with two paths leading to the same work package in the end" do let(:follower3_start_date) { Time.zone.today + 4.days } let(:follower3_due_date) { Time.zone.today + 7.days } let(:follower3_lag) { 0 } @@ -766,12 +766,12 @@ def create_child(parent, start_date, due_date) following_work_package4] end - context 'when moving forward' do + context "when moving forward" do before do work_package.due_date = Time.zone.today + 5.days end - it_behaves_like 'reschedules' do + it_behaves_like "reschedules" do let(:expected) do { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days], following_work_package2 => [Time.zone.today + 9.days, Time.zone.today + 13.days], @@ -781,16 +781,16 @@ def create_child(parent, start_date, due_date) end end - context 'when moving backwards' do + context "when moving backwards" do before do work_package.due_date = Time.zone.today - 5.days end - it_behaves_like 'does not reschedule' + it_behaves_like "does not reschedule" end end - context 'when setting the parent' do + context "when setting the parent" do let(:new_parent_work_package) { create(:work_package) } let(:attributes) { [:parent] } @@ -806,14 +806,14 @@ def create_child(parent, start_date, due_date) context "with the parent being restricted in its ability to be moved" do let(:soonest_date) { Time.zone.today + 3.days } - it 'sets the start date and due date to the earliest possible date' do + it "sets the start date and due date to the earliest possible date" do subject expect(work_package.start_date).to eql(Time.zone.today + 3.days) expect(work_package.due_date).to eql(Time.zone.today + 3.days) end - it 'does not change the due date if after the newly set start date' do + it "does not change the due date if after the newly set start date" do work_package.due_date = Time.zone.today + 5.days subject @@ -822,7 +822,7 @@ def create_child(parent, start_date, due_date) end end - context 'with the parent being restricted but work package already having dates set' do + context "with the parent being restricted but work package already having dates set" do let(:soonest_date) { Time.zone.today + 3.days } before do @@ -830,7 +830,7 @@ def create_child(parent, start_date, due_date) work_package.due_date = Time.zone.today + 5.days end - it 'sets the dates to provided dates' do + it "sets the dates to provided dates" do subject expect(work_package.start_date).to eql(Time.zone.today + 4.days) @@ -838,7 +838,7 @@ def create_child(parent, start_date, due_date) end end - context 'with the parent being restricted but the attributes define an earlier date' do + context "with the parent being restricted but the attributes define an earlier date" do let(:soonest_date) { Time.zone.today + 3.days } before do @@ -848,7 +848,7 @@ def create_child(parent, start_date, due_date) # This would be invalid but the dates should be set nevertheless # so we can have a correct error handling. - it 'sets the dates to provided dates' do + it "sets the dates to provided dates" do subject expect(work_package.start_date).to eql(Time.zone.today + 1.day) @@ -857,7 +857,7 @@ def create_child(parent, start_date, due_date) end end - context 'with deep hierarchy of work packages' do + context "with deep hierarchy of work packages" do before do work_package.due_date = Time.zone.today - 5.days end @@ -872,7 +872,7 @@ def create_hierarchy(parent, nb_children_by_levels) end end - it 'does not fail with a SystemStackError (regression #43894)' do + it "does not fail with a SystemStackError (regression #43894)" do parent = create(:work_package, start_date: Date.current, due_date: Date.current) hierarchy = [1, 1, 1, 1, 2, 4, 4, 4] create_hierarchy(parent, hierarchy) diff --git a/spec/support/components/common/submenu.rb b/spec/support/components/common/submenu.rb index 77c59af075d4..f876bdf700fb 100644 --- a/spec/support/components/common/submenu.rb +++ b/spec/support/components/common/submenu.rb @@ -37,12 +37,13 @@ def expect_item(name, selected: false, visible: true) selected_specifier = selected ? ".selected" : ":not(.selected)" expect(page).to have_css(".op-sidemenu--item-action#{selected_specifier}", text: name, visible:) + # expect(page).to have_css("[data-test-selector='op-sidemenu--item-action']#{selected_specifier}", text: name, visible:) end end def expect_no_item(name) within "#main-menu" do - expect(page).to have_no_css(".op-sidemenu--item-action", text: name) + expect(page).not_to have_test_selector("op-sidemenu--item-action", text: name) end end @@ -52,6 +53,12 @@ def click_item(name) end end + def expect_no_items + within "#main-menu" do + expect(page).not_to have_test_selector("op-sidemenu--item-action") + end + end + def search_for_item(name) within "#main-menu" do page.find_test_selector("op-sidebar--search-input").set(name) diff --git a/spec/support/components/projects/project_custom_fields/edit_dialog.rb b/spec/support/components/projects/project_custom_fields/edit_dialog.rb index 0250315b4441..80abf886e0a7 100644 --- a/spec/support/components/projects/project_custom_fields/edit_dialog.rb +++ b/spec/support/components/projects/project_custom_fields/edit_dialog.rb @@ -26,8 +26,8 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -require 'support/components/common/modal' -require 'support/components/autocompleter/ng_select_autocomplete_helpers' +require "support/components/common/modal" +require "support/components/autocompleter/ng_select_autocomplete_helpers" module Components module Projects @@ -71,7 +71,7 @@ def close def close_via_button within(dialog_css_selector) do - click_link_or_button 'Cancel' + click_link_or_button "Cancel" end end @@ -96,8 +96,8 @@ def expect_async_content_loaded ### def input_containers - within '#project-section-edit-form > .FormControl-spacingWrapper' do - page.all('.FormControl-spacingWrapper') + within "#project-section-edit-form > .FormControl-spacingWrapper" do + page.all(".FormControl-spacingWrapper") end end diff --git a/spec/support/components/work_packages/query_menu.rb b/spec/support/components/work_packages/query_menu.rb index 10f541c862ba..a370d6bb68b4 100644 --- a/spec/support/components/work_packages/query_menu.rb +++ b/spec/support/components/work_packages/query_menu.rb @@ -69,10 +69,6 @@ def expect_menu_entry(name) def expect_menu_entry_not_visible(name) expect(page).to have_no_selector(autocompleter_item_selector, text: name) end - - def expect_no_menu_entry - expect(page).to have_no_selector(autocompleter_item_selector) - end end end end diff --git a/spec/support/components/work_packages/share_modal.rb b/spec/support/components/work_packages/share_modal.rb index edd8ad3f9b24..ab5be93450f2 100644 --- a/spec/support/components/work_packages/share_modal.rb +++ b/spec/support/components/work_packages/share_modal.rb @@ -129,27 +129,27 @@ def expect_select_all_untoggled def expect_bulk_actions_available within shares_header do - expect(page).to have_test_selector("op-share-wp--bulk-remove") - expect(page).to have_test_selector("op-share-wp-bulk-update-role") + expect(page).to have_test_selector("op-share-dialog--bulk-remove") + expect(page).to have_test_selector("op-share-dialog-bulk-update-role") end end def expect_bulk_actions_not_available within shares_header do - expect(page).not_to have_test_selector("op-share-wp--bulk-remove", wait: 0) - expect(page).not_to have_test_selector("op-share-wp-bulk-update-role", wait: 0) + expect(page).not_to have_test_selector("op-share-dialog--bulk-remove", wait: 0) + expect(page).not_to have_test_selector("op-share-dialog-bulk-update-role", wait: 0) end end def bulk_remove within shares_header do - page.find_test_selector("op-share-wp--bulk-remove").click + page.find_test_selector("op-share-dialog--bulk-remove").click end end def bulk_update(role_name) within shares_header do - find('[data-test-selector="op-share-wp-bulk-update-role"]').click + find('[data-test-selector="op-share-dialog-bulk-update-role"]').click find(".ActionListContent", text: role_name).click end @@ -158,7 +158,7 @@ def bulk_update(role_name) def expect_bulk_update_label(label_text) within shares_header do expect(page) - .to have_css('[data-test-selector="op-share-wp-bulk-update-role"] .Button-label', + .to have_css('[data-test-selector="op-share-dialog-bulk-update-role"] .Button-label', text: label_text) if label_text == "Mixed" %w[View Comment Edit].each do |permission_name| @@ -177,7 +177,7 @@ def expect_bulk_update_label(label_text) end def bulk_update_form(permission_name) - find("[data-test-selector='op-share-wp-bulk-update-role-permission-#{permission_name}']", visible: :all) + find("[data-test-selector='op-share-dialog-bulk-update-role-permission-#{permission_name}']", visible: :all) end def checked_permission @@ -190,13 +190,13 @@ def unchecked_permission def expect_blankslate within_modal do - expect(page).to have_text(I18n.t("work_package.sharing.text_empty_state_description")) + expect(page).to have_text(I18n.t("sharing.text_empty_state_description", entity: WorkPackage.model_name.human)) end end def expect_empty_search_blankslate within_modal do - expect(page).to have_text(I18n.t("work_package.sharing.text_empty_search_description")) + expect(page).to have_text(I18n.t("sharing.text_empty_search_description")) end end @@ -213,7 +213,7 @@ def invite_user(users, role_name) select_invite_role(role_name) within_modal do - click_button "Share" + click_on "Share" end end @@ -231,21 +231,21 @@ def invite_user!(user, role_name) end def search_user(search_string) - search_autocomplete page.find('[data-test-selector="op-share-wp-invite-autocomplete"]'), + search_autocomplete page.find('[data-test-selector="op-share-dialog-invite-autocomplete"]'), query: search_string, results_selector: "body" end def remove_user(user) within user_row(user) do - page.find_test_selector("op-share-wp--remove").click + page.find_test_selector("op-share-dialog--remove").click end end def select_invite_role(role_name) - within modal_element.find('[data-test-selector="op-share-wp-invite-role"]') do + within modal_element.find('[data-test-selector="op-share-dialog-invite-role"]') do # Open the ActionMenu - click_button "View" + click_on "View" find(".ActionListContent", text: role_name).click end @@ -253,10 +253,10 @@ def select_invite_role(role_name) def change_role(user, role_name) within user_row(user) do - find('[data-test-selector="op-share-wp-update-role"]').click + find('[data-test-selector="op-share-dialog-update-role"]').click within ".ActionListWrap" do - click_button role_name + click_on role_name end end end @@ -267,7 +267,7 @@ def filter(filter_name, value) # The button's text changes dynamically based on the currently selected option # Hence the spec's readability is hindered by using something like # `click_button filter_name.capitalize` - find("[data-test-selector='op-share-wp-filter-#{filter_name}-button']").click + find("[data-test-selector='op-share-dialog-filter-#{filter_name}-button']").click # Open the ActionMenu find(".ActionListContent", text: value).click @@ -279,13 +279,13 @@ def filter(filter_name, value) def close within_modal do - page.find("[data-test-selector='op-share-wp-modal--close-icon']").click + page.find("[data-test-selector='op-share-dialog-modal--close-icon']").click end end def click_share within_modal do - click_button "Share" + click_on "Share" end end @@ -318,26 +318,26 @@ def expect_not_shared_with(*principals) def expect_shared_count_of(count) expect(shares_header) - .to have_text(I18n.t("work_package.sharing.count", count:)) + .to have_text(I18n.t("sharing.count", count:)) end def expect_no_invite_option within_modal do expect(page) - .to have_text(I18n.t("work_package.sharing.permissions.denied")) + .to have_text(I18n.t("sharing.denied", entities: WorkPackage.model_name.human(count: 2))) end end def resend_invite(user) within user_row(user) do - click_button I18n.t("work_package.sharing.user_details.resend_invite") + click_on I18n.t("sharing.user_details.resend_invite") end end def expect_invite_resent(user) within user_row(user) do expect(page) - .to have_text(I18n.t("work_package.sharing.user_details.invite_resent")) + .to have_text(I18n.t("sharing.user_details.invite_resent")) end end @@ -349,30 +349,30 @@ def user_row(user) def active_list modal_element - .find('[data-test-selector="op-share-wp-active-list"]') + .find('[data-test-selector="op-share-dialog-active-list"]') end def shares_header - active_list.find('[data-test-selector="op-share-wp-header"]') + active_list.find('[data-test-selector="op-share-dialog-header"]') end def shares_counter - shares_header.find('[data-test-selector="op-share-wp-active-count"]') + shares_header.find('[data-test-selector="op-share-dialog-active-count"]') end def shares_list - find_by_id("op-share-wp-active-shares") + find_by_id("op-share-dialog-active-shares") end def select_existing_user(user) - select_autocomplete page.find('[data-test-selector="op-share-wp-invite-autocomplete"]'), + select_autocomplete page.find('[data-test-selector="op-share-dialog-invite-autocomplete"]'), query: user.firstname, select_text: user.name, results_selector: "body" end def select_not_existing_user_option(email) - select_autocomplete page.find('[data-test-selector="op-share-wp-invite-autocomplete"]'), + select_autocomplete page.find('[data-test-selector="op-share-dialog-invite-autocomplete"]'), query: email, select_text: "Send invite to\"#{email}\"", results_selector: "body" @@ -388,21 +388,21 @@ def expect_upsale_banner def expect_no_user_limit_warning within modal_element do expect(page) - .to have_no_text(I18n.t("work_package.sharing.warning_user_limit_reached"), wait: 0) + .to have_no_text(I18n.t("sharing.warning_user_limit_reached", entity: WorkPackage.model_name.human), wait: 0) end end def expect_user_limit_warning within modal_element do expect(page) - .to have_text(I18n.t("work_package.sharing.warning_user_limit_reached")) + .to have_text(I18n.t("sharing.warning_user_limit_reached", entity: WorkPackage.model_name.human)) end end def expect_error_message(text) within modal_element do expect(page) - .to have_css('[data-test-selector="op-share-wp-error-message"]', + .to have_css('[data-test-selector="op-share-dialog-error-message"]', text:) end end @@ -410,14 +410,14 @@ def expect_error_message(text) def expect_select_a_user_hint within modal_element do expect(page) - .to have_text(I18n.t("work_package.sharing.warning_no_selected_user")) + .to have_text(I18n.t("sharing.warning_no_selected_user", entity: WorkPackage.model_name.human)) end end def expect_no_select_a_user_hint within modal_element do expect(page) - .to have_no_text(I18n.t("work_package.sharing.warning_no_selected_user"), wait: 0) + .to have_no_text(I18n.t("sharing.warning_no_selected_user", entity: WorkPackage.model_name.human), wait: 0) end end end diff --git a/spec/support/have_http_status_with_rack_response.rb b/spec/support/have_http_status_with_rack_response.rb new file mode 100644 index 000000000000..f933d04d1b90 --- /dev/null +++ b/spec/support/have_http_status_with_rack_response.rb @@ -0,0 +1,44 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module HaveHttpStatusWithRackResponse + def as_test_response(obj) + if obj.is_a?(Rack::MockResponse) + # `have_http_status` matcher would fail if the response object is a + # `Rack::MockResponse`. Hack to disguise `Rack::MockResponse` into a + # `ActionDispatch::TestResponse` object. + response = ActionDispatch::Response.new(obj.status, obj.headers, obj.body) + response.request = ActionDispatch::Request.new({}) + ::ActionDispatch::TestResponse.from_response(response) + else + super + end + end +end + +RSpec::Rails::Matchers::HaveHttpStatus.prepend(HaveHttpStatusWithRackResponse) diff --git a/spec/support/pages/notifications/center.rb b/spec/support/pages/notifications/center.rb index 6c0c0d7ac80f..f1b5c8283410 100644 --- a/spec/support/pages/notifications/center.rb +++ b/spec/support/pages/notifications/center.rb @@ -40,7 +40,7 @@ def path end def mark_all_read - click_button 'Mark all as read' + click_button "Mark all as read" end def mark_notification_as_read(notification) @@ -50,13 +50,13 @@ def mark_notification_as_read(notification) end def show_all - click_button 'All' + click_button "All" end def item_title(notification) text = notification.resource.is_a?(WorkPackage) ? notification.resource.subject : notification.subject within_item(notification) do - page.find('span', text:, exact_text: true) + page.find("span", text:, exact_text: true) end end @@ -111,15 +111,15 @@ def expect_work_package_item(*notifications) end def expect_closed - expect(page).to have_no_css('op-in-app-notification-center') + expect(page).to have_no_css("op-in-app-notification-center") end def expect_open - expect(page).to have_css('op-in-app-notification-center') + expect(page).to have_css("op-in-app-notification-center") end def expect_empty - expect(page).to have_text 'New notifications will appear here when there is activity that concerns you' + expect(page).to have_text "New notifications will appear here when there is activity that concerns you" end def expect_number_of_notifications(count) @@ -143,15 +143,15 @@ def bell_element end def expect_no_toaster - expect(page).to have_no_css('.op-toast.-info', wait: 10) + expect(page).to have_no_css(".op-toast.-info", wait: 10) end def expect_toast - expect(page).to have_css('.op-toast.-info', wait: 10) + expect(page).to have_css(".op-toast.-info", wait: 10) end def update_via_toaster - page.find('.op-toast.-info a', wait: 10).click + page.find(".op-toast.-info a", wait: 10).click end end end diff --git a/spec/support/queries/shared_get_individual_query_examples.rb b/spec/support/queries/shared_get_individual_query_examples.rb index ad7ee08b3720..0de710dbeced 100644 --- a/spec/support/queries/shared_get_individual_query_examples.rb +++ b/spec/support/queries/shared_get_individual_query_examples.rb @@ -44,7 +44,7 @@ end it "succeeds" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end it "has the right endpoint set for the self reference" do @@ -140,7 +140,7 @@ context "with EE", with_ee: %i[baseline_comparison] do it "succeeds" do - expect(last_response.status).to eq(200) + expect(last_response).to have_http_status(:ok) end end diff --git a/spec/workers/principals/delete_job_integration_spec.rb b/spec/workers/principals/delete_job_integration_spec.rb index 2837477f3253..82a8dab14142 100644 --- a/spec/workers/principals/delete_job_integration_spec.rb +++ b/spec/workers/principals/delete_job_integration_spec.rb @@ -317,7 +317,7 @@ it "removes the query" do job - expect(Queries::Projects::ProjectQuery.find_by(id: query.id)).to be_nil + expect(ProjectQuery.find_by(id: query.id)).to be_nil end end