diff --git a/config/initializers/menus.rb b/config/initializers/menus.rb index 74b281889850..6d5a3d9bc8d3 100644 --- a/config/initializers/menus.rb +++ b/config/initializers/menus.rb @@ -68,7 +68,7 @@ OpenProject::Static::Links.help_link, last: true, caption: '', - icon: 'icon-help op-app-help--icon', + icon: 'help op-app-help--icon', html: { accesskey: OpenProject::AccessKeys.key_for(:help), title: I18n.t('label_help'), target: '_blank' } diff --git a/docs/development/data-flow/README.md b/docs/development/data-flow/README.md index 072a865ef122..bd2811fc3e2f 100644 --- a/docs/development/data-flow/README.md +++ b/docs/development/data-flow/README.md @@ -1,4 +1,4 @@ -# Data flow and Usage +# Data flow and usage Regardless of the type of installation of OpenProject, the following diagram provides a high-level overview of through which systems data related to OpenProject is flowing. diff --git a/docs/getting-started/my-activity/README.md b/docs/getting-started/my-activity/README.md index 1c2d87061324..f96040114b07 100644 --- a/docs/getting-started/my-activity/README.md +++ b/docs/getting-started/my-activity/README.md @@ -18,7 +18,9 @@ You will see two lists by default. **Projects** will show all projects you are a member of. -**Activity** will show all of your activities that are being recorded in OpenProject. Note that only activities from projects that have enabled the "Activity" module will be shown. +**Activity** will show all of your activities that are being recorded in OpenProject. + +>Please note that only activities from projects that have the **Activity** module enabled will be shown. ![Openproject_my_activity_page](openproject_my_activity_overview.png) diff --git a/docs/release-notes/13-0-7/README.md b/docs/release-notes/13-0-7/README.md new file mode 100644 index 000000000000..e51224bec30f --- /dev/null +++ b/docs/release-notes/13-0-7/README.md @@ -0,0 +1,27 @@ +--- +title: OpenProject 13.0.7 +sidebar_navigation: + title: 13.0.7 +release_version: 13.0.7 +release_date: 2023-10-23 +--- + +# OpenProject 13.0.7 + +Release date: 2023-10-23 + +We released [OpenProject 13.0.7](https://community.openproject.com/versions/1938). +The release contains several bug fixes and we recommend updating to the newest version. + + +#### Bug fixes and changes + +- Fixed: File Drag and Drop \[[#49507](https://community.openproject.com/wp/49507)\] +- Fixed: Help icon not shown when having a custom help link setting \[[#50666](https://community.openproject.com/wp/50666)\] + +#### Contributions +A big thanks to community members for reporting bugs and helping us identifying and providing fixes. + +Special thanks for reporting and finding bugs go to + +Patrick Stapf diff --git a/docs/release-notes/README.md b/docs/release-notes/README.md index 78523647d263..20ed43d5c0d6 100644 --- a/docs/release-notes/README.md +++ b/docs/release-notes/README.md @@ -14,6 +14,13 @@ Stay up to date and get an overview of the new features included in the releases +## 13.0.7 + +Release date: 2023-10-23 + +[Release Notes](13-0-7/) + + ## 13.0.6 Release date: 2023-10-13 diff --git a/frontend/src/app/shared/components/attachments/attachments.component.ts b/frontend/src/app/shared/components/attachments/attachments.component.ts index da917fe50b2b..a0e3848fb813 100644 --- a/frontend/src/app/shared/components/attachments/attachments.component.ts +++ b/frontend/src/app/shared/components/attachments/attachments.component.ts @@ -113,7 +113,13 @@ export class OpAttachmentsComponent extends UntilDestroyedMixin implements OnIni }; private onGlobalDragEnter:(_event:DragEvent) => void = (_event) => { - this.dragging += 1; + // When the global drag and drop is active and the dragging happens over the DOM + // elements, the dragenter and dragleave events are always fired in pairs. + // On dragenter the this.dragging is set to 2 and on dragleave we deduct it to 1, + // meaning the drag and drop remains active. When the drag and drop action is canceled + // i.e. by the "Escape" key, an extra dragleave event is fired. + // In this case this.dragging will be deducted to 0, disabling the active drop areas. + this.dragging = 2; this.cdRef.detectChanges(); }; diff --git a/frontend/src/app/shared/components/editor/components/ckeditor/op-ckeditor.component.ts b/frontend/src/app/shared/components/editor/components/ckeditor/op-ckeditor.component.ts index 02fa45e1c8f1..dc13f6fa0136 100644 --- a/frontend/src/app/shared/components/editor/components/ckeditor/op-ckeditor.component.ts +++ b/frontend/src/app/shared/components/editor/components/ckeditor/op-ckeditor.component.ts @@ -232,13 +232,6 @@ export class OpCkeditorComponent implements OnInit, OnDestroy { model.on('op:attachment-added', () => document.body.dispatchEvent(new DragEvent('dragend'))); model.on('op:attachment-removed', () => document.body.dispatchEvent(new DragEvent('dragend'))); - // Emitting a global dragleave on every dragleave of the ckeditor element - // IMPORTANT: This emits much more dragleave events then dragenter events. - // In the end, this leads to a break in every drop zone that listens to those two global events - // to determine its state. Without it, if no dragleave is fired, the drop zones enter a failed state, - // not vanishing after ending the drag. - this.$element.on('dragleave', () => document.body.dispatchEvent(new DragEvent('dragleave'))); - this.initializeDone.emit(watchdog.editor); return watchdog.editor; }); diff --git a/frontend/src/app/shared/components/storages/storage/storage.component.ts b/frontend/src/app/shared/components/storages/storage/storage.component.ts index 36efddd44129..4919c1efbe34 100644 --- a/frontend/src/app/shared/components/storages/storage/storage.component.ts +++ b/frontend/src/app/shared/components/storages/storage/storage.component.ts @@ -189,7 +189,13 @@ export class StorageComponent extends UntilDestroyedMixin implements OnInit, OnD }; private onGlobalDragEnter:(_event:DragEvent) => void = (_event) => { - this.dragging += 1; + // When the global drag and drop is active and the dragging happens over the DOM + // elements, the dragenter and dragleave events are always fired in pairs. + // On dragenter the this.dragging is set to 2 and on dragleave we deduct it to 1, + // meaning the drag and drop remains active. When the drag and drop action is canceled + // i.e. by the "Escape" key, an extra dragleave event is fired. + // In this case this.dragging will be deducted to 0, disabling the active drop areas. + this.dragging = 2; this.cdRef.detectChanges(); }; diff --git a/frontend/src/global_styles/common/header/app-help.sass b/frontend/src/global_styles/common/header/app-help.sass index b18f0f9c524f..57acde51d631 100644 --- a/frontend/src/global_styles/common/header/app-help.sass +++ b/frontend/src/global_styles/common/header/app-help.sass @@ -1,5 +1,6 @@ .op-app-help - &--icon + // For the higher specificity we switch to this nested class notation + .op-app-help--icon &::before display: flex justify-content: center diff --git a/lib/open_project/version.rb b/lib/open_project/version.rb index 1c5645415d88..50e0f4b62947 100644 --- a/lib/open_project/version.rb +++ b/lib/open_project/version.rb @@ -33,7 +33,7 @@ module OpenProject module VERSION # :nodoc: MAJOR = 13 MINOR = 0 - PATCH = 6 + PATCH = 7 class << self # Used by semver to define the special version (if any). diff --git a/lib/redmine/menu_manager/top_menu/help_menu.rb b/lib/redmine/menu_manager/top_menu/help_menu.rb index 4b088d72f44e..3a8c4df6ff37 100644 --- a/lib/redmine/menu_manager/top_menu/help_menu.rb +++ b/lib/redmine/menu_manager/top_menu/help_menu.rb @@ -49,7 +49,7 @@ def render_help_dropdown title: I18n.t(:label_help), class: 'op-app-menu--item-action', aria: { haspopup: 'true' } do - op_icon('icon-help op-app-help--icon') + spot_icon('help', size: '1_25', classnames: 'op-app-help--icon') end render_menu_dropdown( diff --git a/modules/avatars/config/locales/crowdin/az.yml b/modules/avatars/config/locales/crowdin/az.yml index 18037cdce663..0042df653ea2 100644 --- a/modules/avatars/config/locales/crowdin/az.yml +++ b/modules/avatars/config/locales/crowdin/az.yml @@ -3,39 +3,39 @@ az: plugin_openproject_avatars: name: "Avatarlar" description: >- - This plugin allows OpenProject users to upload a picture to be used as an avatar or use registered images from Gravatar. + Bu plagin OpenProject istifadəçilərinə avatar kimi istifadə edilmək üçün şəkil yükləməyə və ya Gravatar-dan qeydə alınmış şəkillərdən istifadə etməyə imkan verir. label_avatar: "Avatar" label_avatar_plural: "Avatarlar" label_current_avatar: "Cari avatar" - label_choose_avatar: "Choose Avatar from file" - message_avatar_uploaded: "Avatar changed successfully." - error_image_upload: "Error saving the image." - error_image_size: "The image is too large." - button_change_avatar: "Change avatar" - are_you_sure_delete_avatar: "Are you sure you want to delete your avatar?" - avatar_deleted: "Avatar deleted successfully." - unable_to_delete_avatar: "Avatar could not be deleted." - wrong_file_format: "Allowed formats are jpg, png, gif" - empty_file_error: "Please upload a valid image (jpg, png, gif)" + label_choose_avatar: "Avatar seçin" + message_avatar_uploaded: "Avatar uğurla dəyişdirildi" + error_image_upload: "Şəkli yadda saxlama xətası" + error_image_size: "Şəkil çox böyükdür" + button_change_avatar: "Avatarı dəyişdirin" + are_you_sure_delete_avatar: "Avatarınızı silmək istədiyinizə əminsiniz?" + avatar_deleted: "Avatar uğurla silindi." + unable_to_delete_avatar: "Avatar silinmədi." + wrong_file_format: "İcazə verilən formatlar jpg, png, gif-dir" + empty_file_error: "Zəhmət olmasa düzgün şəkil yükləyin (jpg, png, gif)" avatars: label_avatar: "Avatar" label_gravatar: 'Gravatar' label_current_avatar: 'Cari avatar' label_local_avatar: 'Fərdi avatar' text_current_avatar: | - The following image shows the current avatar. + Aşağıdakı şəkil cari avatar olduğunu göstərir. text_upload_instructions: | - Upload your own custom avatar of 128 by 128 pixels. Larger files will be resized and cropped to match. - A preview of your avatar will be shown before uploading, once you selected an image. - text_change_gravatar_html: 'To change or add the Gravatar for your mail address, go to %{gravatar_url}.' + 128x128 piksel ölçüsündə öz fərdi avatarınızı yükləyin. Daha böyük faylların ölçüsü dəyişdiriləcək və uyğunlaşmaq üçün kəsiləcək. + Siz şəkil seçdikdən sonra, avatarınız yükləməzdən əvvəl sızə göstəriləcək + text_change_gravatar_html: 'Poçt ünvanınızın Gravatarını dəyişmək və ya əlavə etmək üçün %{gravatar_url} ünvanına keçin.' text_your_local_avatar: | - OpenProject allows you to upload your own custom avatar. + OpenProject sizə öz fərdi avatarınızı yükləməyə imkan verir. text_local_avatar_over_gravatar: | - If you set one, this custom avatar is used in precedence over the gravatar above. + Əgər birini təyin etsəniz, bu fərdi avatar yuxarıdakı qravatardan üstün olaraq istifadə olunur. text_your_current_gravatar: | - OpenProject uses your gravatar if you registered one, or a default image or icon if one exists. - The current gravatar is as follows: + OpenProject qravatarınızdan, əgər varsa, defolt şəkil və ya ikonadan istifadə edir. + Hazırkı qravatar aşağıdakı kimidir settings: - enable_gravatars: 'Enable user gravatars' - gravatar_default: "Default Gravatar image" - enable_local_avatars: 'Enable user custom avatars' + enable_gravatars: 'İstifadəçi qravatarlarını aktivləşdirin' + gravatar_default: "Defolt Gravatar şəkli" + enable_local_avatars: 'İstifadəçinin fərdi avatarlarını aktivləşdirin' diff --git a/modules/avatars/config/locales/crowdin/js-az.yml b/modules/avatars/config/locales/crowdin/js-az.yml index fd569287335f..bf0c33d2699a 100644 --- a/modules/avatars/config/locales/crowdin/js-az.yml +++ b/modules/avatars/config/locales/crowdin/js-az.yml @@ -4,12 +4,12 @@ az: label_preview: 'İlkin baxılış' button_update: 'Yeniləmə' avatars: - label_choose_avatar: "Choose Avatar from file" - uploading_avatar: "Uploading your avatar." + label_choose_avatar: "Avatar seçin" + uploading_avatar: "Avatarınız yüklənir" text_upload_instructions: | - Upload your own custom avatar of 128 by 128 pixels. Larger files will be resized and cropped to match. - A preview of your avatar will be shown before uploading, once you selected an image. - error_image_too_large: "Image is too large." - wrong_file_format: "Allowed formats are jpg, png, gif" - empty_file_error: "Please upload a valid image (jpg, png, gif)" + 128x128 piksel ölçüsündə öz fərdi avatarınızı yükləyin. Daha böyük faylların ölçüsü dəyişdiriləcək və uyğunlaşmaq üçün kəsiləcək. + Siz şəkil seçdikdən sonra, avatarınız yükləməzdən əvvəl sızə göstəriləcək + error_image_too_large: "Şəkil çox böyükdür" + wrong_file_format: "İcazə verilən formatlar jpg, png, gif-dir" + empty_file_error: "Zəhmət olmasa düzgün şəkil yükləyin (jpg, png, gif)" diff --git a/modules/backlogs/config/locales/crowdin/az.yml b/modules/backlogs/config/locales/crowdin/az.yml index 2641b3cbf942..683c7aa8a615 100644 --- a/modules/backlogs/config/locales/crowdin/az.yml +++ b/modules/backlogs/config/locales/crowdin/az.yml @@ -27,7 +27,7 @@ az: attributes: work_package: position: "Vəzifə" - remaining_hours: "Remaining hours" + remaining_hours: "Qalan vaxt %d saat" remaining_time: "Remaining hours" derived_remaining_hours: "Derived remaining hours" derived_remaining_time: "Derived remaining hours" diff --git a/modules/storages/app/contracts/storages/project_storages/base_contract.rb b/modules/storages/app/contracts/storages/project_storages/base_contract.rb index 08faa3abed12..896572111781 100644 --- a/modules/storages/app/contracts/storages/project_storages/base_contract.rb +++ b/modules/storages/app/contracts/storages/project_storages/base_contract.rb @@ -52,10 +52,20 @@ class BaseContract < ::ModelContract end end + validate :project_folder_automatic_mode, unless: -> { errors.include?(:project_folder_mode) } + private def project_folder_mode_manual? @model.project_folder_manual? end + + def project_folder_automatic_mode + return unless @model.project_folder_automatic? + + unless @model.automatic_management_possible? + errors.add :project_folder_mode, :mode_unavailable + end + end end end diff --git a/modules/storages/app/models/storages/project_storage.rb b/modules/storages/app/models/storages/project_storage.rb index 36cd29fc5739..89b8a90c25e7 100644 --- a/modules/storages/app/models/storages/project_storage.rb +++ b/modules/storages/app/models/storages/project_storage.rb @@ -51,6 +51,10 @@ class Storages::ProjectStorage < ApplicationRecord scope :automatic, -> { where(project_folder_mode: 'automatic') } + def automatic_management_possible? + storage.present? && storage.provider_type_nextcloud? && storage.automatically_managed? + end + def project_folder_path "#{storage.group_folder}/#{project.name.gsub('/', '|')} (#{project.id})/" end diff --git a/modules/storages/app/views/storages/project_settings/_project_folder_form.html.erb b/modules/storages/app/views/storages/project_settings/_project_folder_form.html.erb index 2a06dde01bbc..8fdadbb09775 100644 --- a/modules/storages/app/views/storages/project_settings/_project_folder_form.html.erb +++ b/modules/storages/app/views/storages/project_settings/_project_folder_form.html.erb @@ -88,18 +88,20 @@ See COPYRIGHT and LICENSE files for more details. <%= t(:"storages.instructions.no_specific_folder") %> -
- <%= f.label :project_folder_mode, class: "form--label-with-radio-button" do %> - <%= f.radio_button :project_folder_mode, - 'automatic', - no_label: true, - data: { action: 'project-storage-form#updateForm' } %> - <%= t(:"storages.label_automatic_folder") %> - <% end %> -
- - <%= t(:"storages.instructions.automatic_folder") %> - + <% if @project_storage.automatic_management_possible? %> +
+ <%= f.label :project_folder_mode, class: "form--label-with-radio-button" do %> + <%= f.radio_button :project_folder_mode, + 'automatic', + no_label: true, + data: { action: 'project-storage-form#updateForm' } %> + <%= t(:"storages.label_automatic_folder") %> + <% end %> +
+ + <%= t(:"storages.instructions.automatic_folder") %> + + <% end %>
<%= f.label :project_folder_mode, class: "form--label-with-radio-button" do %> diff --git a/modules/storages/app/views/storages/project_settings/edit.html.erb b/modules/storages/app/views/storages/project_settings/edit.html.erb index 38a69a3438ba..ea578622da67 100644 --- a/modules/storages/app/views/storages/project_settings/edit.html.erb +++ b/modules/storages/app/views/storages/project_settings/edit.html.erb @@ -31,6 +31,8 @@ See COPYRIGHT and LICENSE files for more details. <% local_assigns[:additional_breadcrumb] = t('storages.label_edit_storage') %> <%= toolbar title: t("storages.page_titles.project_settings.edit") %> +<%= error_messages_for_contract @project_storage, @errors %> + <%= labelled_tabular_form_for @project_storage, url: project_settings_project_storage_path(project_id: @project_storage.project, id: @project_storage) do |f| -%>
<%= render partial: '/storages/project_settings/project_folder_form', diff --git a/modules/storages/config/locales/crowdin/af.yml b/modules/storages/config/locales/crowdin/af.yml index bb37ac62f2b1..3ee135e0024f 100644 --- a/modules/storages/config/locales/crowdin/af.yml +++ b/modules/storages/config/locales/crowdin/af.yml @@ -32,6 +32,10 @@ af: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/ar.yml b/modules/storages/config/locales/crowdin/ar.yml index 5e695f3ad06b..2a974e5e631a 100644 --- a/modules/storages/config/locales/crowdin/ar.yml +++ b/modules/storages/config/locales/crowdin/ar.yml @@ -32,6 +32,10 @@ ar: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/az.yml b/modules/storages/config/locales/crowdin/az.yml index 1183d6f05794..1da66f067cc7 100644 --- a/modules/storages/config/locales/crowdin/az.yml +++ b/modules/storages/config/locales/crowdin/az.yml @@ -32,6 +32,10 @@ az: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/be.yml b/modules/storages/config/locales/crowdin/be.yml index 9e0576f1681e..3273820d3fc1 100644 --- a/modules/storages/config/locales/crowdin/be.yml +++ b/modules/storages/config/locales/crowdin/be.yml @@ -32,6 +32,10 @@ be: messages: not_linked_to_project: "не звязаны з праектам." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/bg.yml b/modules/storages/config/locales/crowdin/bg.yml index bca705df8828..e2dd7a5cb811 100644 --- a/modules/storages/config/locales/crowdin/bg.yml +++ b/modules/storages/config/locales/crowdin/bg.yml @@ -32,6 +32,10 @@ bg: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/ca.yml b/modules/storages/config/locales/crowdin/ca.yml index 1e66d1d91ce7..a13f9159ee2b 100644 --- a/modules/storages/config/locales/crowdin/ca.yml +++ b/modules/storages/config/locales/crowdin/ca.yml @@ -32,6 +32,10 @@ ca: messages: not_linked_to_project: "encara no està enllaçat a un projecte." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/ckb-IR.yml b/modules/storages/config/locales/crowdin/ckb-IR.yml index f9201fc28e52..b08873e3825e 100644 --- a/modules/storages/config/locales/crowdin/ckb-IR.yml +++ b/modules/storages/config/locales/crowdin/ckb-IR.yml @@ -32,6 +32,10 @@ ckb-IR: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/cs.yml b/modules/storages/config/locales/crowdin/cs.yml index e552b30c9b65..fe5716c52e0a 100644 --- a/modules/storages/config/locales/crowdin/cs.yml +++ b/modules/storages/config/locales/crowdin/cs.yml @@ -32,6 +32,10 @@ cs: messages: not_linked_to_project: "není propojen s projektem." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/da.yml b/modules/storages/config/locales/crowdin/da.yml index 1017d3f2e242..b7ac998337c1 100644 --- a/modules/storages/config/locales/crowdin/da.yml +++ b/modules/storages/config/locales/crowdin/da.yml @@ -32,6 +32,10 @@ da: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/de.yml b/modules/storages/config/locales/crowdin/de.yml index cc2f3231c0c2..7fac94528f56 100644 --- a/modules/storages/config/locales/crowdin/de.yml +++ b/modules/storages/config/locales/crowdin/de.yml @@ -32,6 +32,10 @@ de: messages: not_linked_to_project: "ist nicht mit dem Projekt verknüpft." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/el.yml b/modules/storages/config/locales/crowdin/el.yml index a927a89a7fa8..16e72da5ee6b 100644 --- a/modules/storages/config/locales/crowdin/el.yml +++ b/modules/storages/config/locales/crowdin/el.yml @@ -32,6 +32,10 @@ el: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/eo.yml b/modules/storages/config/locales/crowdin/eo.yml index 1a05fe5db71b..aa15f74e6569 100644 --- a/modules/storages/config/locales/crowdin/eo.yml +++ b/modules/storages/config/locales/crowdin/eo.yml @@ -32,6 +32,10 @@ eo: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/es.yml b/modules/storages/config/locales/crowdin/es.yml index 7131a122796d..2ad387ad4043 100644 --- a/modules/storages/config/locales/crowdin/es.yml +++ b/modules/storages/config/locales/crowdin/es.yml @@ -32,6 +32,10 @@ es: messages: not_linked_to_project: "no está vinculado al proyecto." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/et.yml b/modules/storages/config/locales/crowdin/et.yml index 25a5ffa7e1f4..bf17203a2153 100644 --- a/modules/storages/config/locales/crowdin/et.yml +++ b/modules/storages/config/locales/crowdin/et.yml @@ -32,6 +32,10 @@ et: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/eu.yml b/modules/storages/config/locales/crowdin/eu.yml index 25212082fc28..37fd4b699509 100644 --- a/modules/storages/config/locales/crowdin/eu.yml +++ b/modules/storages/config/locales/crowdin/eu.yml @@ -32,6 +32,10 @@ eu: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/fa.yml b/modules/storages/config/locales/crowdin/fa.yml index 3e4838cc76ed..96ca9558ea3d 100644 --- a/modules/storages/config/locales/crowdin/fa.yml +++ b/modules/storages/config/locales/crowdin/fa.yml @@ -32,6 +32,10 @@ fa: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/fi.yml b/modules/storages/config/locales/crowdin/fi.yml index 6d153bea9b73..5f920f885725 100644 --- a/modules/storages/config/locales/crowdin/fi.yml +++ b/modules/storages/config/locales/crowdin/fi.yml @@ -32,6 +32,10 @@ fi: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/fil.yml b/modules/storages/config/locales/crowdin/fil.yml index e806ba578df0..c69889a541d9 100644 --- a/modules/storages/config/locales/crowdin/fil.yml +++ b/modules/storages/config/locales/crowdin/fil.yml @@ -32,6 +32,10 @@ fil: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/fr.yml b/modules/storages/config/locales/crowdin/fr.yml index 4042376ad4b1..e74bb88c1494 100644 --- a/modules/storages/config/locales/crowdin/fr.yml +++ b/modules/storages/config/locales/crowdin/fr.yml @@ -32,6 +32,10 @@ fr: messages: not_linked_to_project: "n'est pas lié au projet." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/he.yml b/modules/storages/config/locales/crowdin/he.yml index 4a2d94cd2a0d..0df5745e8c82 100644 --- a/modules/storages/config/locales/crowdin/he.yml +++ b/modules/storages/config/locales/crowdin/he.yml @@ -32,6 +32,10 @@ he: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/hi.yml b/modules/storages/config/locales/crowdin/hi.yml index 467135c82473..a1db9144a246 100644 --- a/modules/storages/config/locales/crowdin/hi.yml +++ b/modules/storages/config/locales/crowdin/hi.yml @@ -32,6 +32,10 @@ hi: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/hr.yml b/modules/storages/config/locales/crowdin/hr.yml index 01a0fc6ed2b7..572c4f2c3218 100644 --- a/modules/storages/config/locales/crowdin/hr.yml +++ b/modules/storages/config/locales/crowdin/hr.yml @@ -32,6 +32,10 @@ hr: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/hu.yml b/modules/storages/config/locales/crowdin/hu.yml index 8fea4daec800..aab264a26cf4 100644 --- a/modules/storages/config/locales/crowdin/hu.yml +++ b/modules/storages/config/locales/crowdin/hu.yml @@ -32,6 +32,10 @@ hu: messages: not_linked_to_project: "nincs projekthez kapcsolva." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/id.yml b/modules/storages/config/locales/crowdin/id.yml index 62ca35510228..07ee367ffca3 100644 --- a/modules/storages/config/locales/crowdin/id.yml +++ b/modules/storages/config/locales/crowdin/id.yml @@ -32,6 +32,10 @@ id: messages: not_linked_to_project: "tidak terhubungi ke proyek." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/it.yml b/modules/storages/config/locales/crowdin/it.yml index 3118007ee743..485c8b879a08 100644 --- a/modules/storages/config/locales/crowdin/it.yml +++ b/modules/storages/config/locales/crowdin/it.yml @@ -32,6 +32,10 @@ it: messages: not_linked_to_project: "non è collegato al progetto." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/ja.yml b/modules/storages/config/locales/crowdin/ja.yml index b154273b4fd4..4ce5934c1735 100644 --- a/modules/storages/config/locales/crowdin/ja.yml +++ b/modules/storages/config/locales/crowdin/ja.yml @@ -32,6 +32,10 @@ ja: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/ka.yml b/modules/storages/config/locales/crowdin/ka.yml index 5468bf8ed088..fcfbd36a5d58 100644 --- a/modules/storages/config/locales/crowdin/ka.yml +++ b/modules/storages/config/locales/crowdin/ka.yml @@ -32,6 +32,10 @@ ka: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/ko.yml b/modules/storages/config/locales/crowdin/ko.yml index 182d8a90dbd7..5b125728c911 100644 --- a/modules/storages/config/locales/crowdin/ko.yml +++ b/modules/storages/config/locales/crowdin/ko.yml @@ -32,6 +32,10 @@ ko: messages: not_linked_to_project: "- 프로젝트에 연결되지 않았습니다." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/lt.yml b/modules/storages/config/locales/crowdin/lt.yml index 749848de2aac..2430839b3a09 100644 --- a/modules/storages/config/locales/crowdin/lt.yml +++ b/modules/storages/config/locales/crowdin/lt.yml @@ -32,6 +32,10 @@ lt: messages: not_linked_to_project: "nesusietas su projektu." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "negalimas šiai saugyklai." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/lv.yml b/modules/storages/config/locales/crowdin/lv.yml index 28391448d9c1..1a2c85ece3de 100644 --- a/modules/storages/config/locales/crowdin/lv.yml +++ b/modules/storages/config/locales/crowdin/lv.yml @@ -32,6 +32,10 @@ lv: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/mn.yml b/modules/storages/config/locales/crowdin/mn.yml index a2fa765179f6..11fcdd810515 100644 --- a/modules/storages/config/locales/crowdin/mn.yml +++ b/modules/storages/config/locales/crowdin/mn.yml @@ -32,6 +32,10 @@ mn: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/ne.yml b/modules/storages/config/locales/crowdin/ne.yml index 51b2b1ad0ce1..40c17f3e3fde 100644 --- a/modules/storages/config/locales/crowdin/ne.yml +++ b/modules/storages/config/locales/crowdin/ne.yml @@ -32,6 +32,10 @@ ne: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/nl.yml b/modules/storages/config/locales/crowdin/nl.yml index 74eff11f7e82..325e75e0cd27 100644 --- a/modules/storages/config/locales/crowdin/nl.yml +++ b/modules/storages/config/locales/crowdin/nl.yml @@ -32,6 +32,10 @@ nl: messages: not_linked_to_project: "is niet gekoppeld aan het project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/no.yml b/modules/storages/config/locales/crowdin/no.yml index 87ec4ec47b0a..cb1f2758b6d5 100644 --- a/modules/storages/config/locales/crowdin/no.yml +++ b/modules/storages/config/locales/crowdin/no.yml @@ -32,6 +32,10 @@ messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/pl.yml b/modules/storages/config/locales/crowdin/pl.yml index 5a50497b4191..c9f379403dff 100644 --- a/modules/storages/config/locales/crowdin/pl.yml +++ b/modules/storages/config/locales/crowdin/pl.yml @@ -32,6 +32,10 @@ pl: messages: not_linked_to_project: "nie ma powiązania z projektem." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/pt.yml b/modules/storages/config/locales/crowdin/pt.yml index 36c96850b059..0e0dfa672992 100644 --- a/modules/storages/config/locales/crowdin/pt.yml +++ b/modules/storages/config/locales/crowdin/pt.yml @@ -32,6 +32,10 @@ pt: messages: not_linked_to_project: "não está vinculado ao projeto." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/ro.yml b/modules/storages/config/locales/crowdin/ro.yml index 546a081377c9..1e9364fa4659 100644 --- a/modules/storages/config/locales/crowdin/ro.yml +++ b/modules/storages/config/locales/crowdin/ro.yml @@ -32,6 +32,10 @@ ro: messages: not_linked_to_project: "nu este legat de proiect." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/ru.yml b/modules/storages/config/locales/crowdin/ru.yml index 1d8540263fb7..6afad9cbe86d 100644 --- a/modules/storages/config/locales/crowdin/ru.yml +++ b/modules/storages/config/locales/crowdin/ru.yml @@ -32,6 +32,10 @@ ru: messages: not_linked_to_project: "не связан с проектом." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/rw.yml b/modules/storages/config/locales/crowdin/rw.yml index 96a54aa8cbf0..a97c585904a6 100644 --- a/modules/storages/config/locales/crowdin/rw.yml +++ b/modules/storages/config/locales/crowdin/rw.yml @@ -32,6 +32,10 @@ rw: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/si.yml b/modules/storages/config/locales/crowdin/si.yml index 4a410231c8bf..dcd63e32ea64 100644 --- a/modules/storages/config/locales/crowdin/si.yml +++ b/modules/storages/config/locales/crowdin/si.yml @@ -32,6 +32,10 @@ si: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/sk.yml b/modules/storages/config/locales/crowdin/sk.yml index d2e149ef5f8d..f5b414fb7953 100644 --- a/modules/storages/config/locales/crowdin/sk.yml +++ b/modules/storages/config/locales/crowdin/sk.yml @@ -32,6 +32,10 @@ sk: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/sl.yml b/modules/storages/config/locales/crowdin/sl.yml index 67768559bcd3..d6b6164792ae 100644 --- a/modules/storages/config/locales/crowdin/sl.yml +++ b/modules/storages/config/locales/crowdin/sl.yml @@ -32,6 +32,10 @@ sl: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/sr.yml b/modules/storages/config/locales/crowdin/sr.yml index 4076ef86aded..8beff7505591 100644 --- a/modules/storages/config/locales/crowdin/sr.yml +++ b/modules/storages/config/locales/crowdin/sr.yml @@ -32,6 +32,10 @@ sr: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/sv.yml b/modules/storages/config/locales/crowdin/sv.yml index 4c9af1af1d00..9af192d5bc67 100644 --- a/modules/storages/config/locales/crowdin/sv.yml +++ b/modules/storages/config/locales/crowdin/sv.yml @@ -32,6 +32,10 @@ sv: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/th.yml b/modules/storages/config/locales/crowdin/th.yml index a3cbebbab7e3..01d593993cfe 100644 --- a/modules/storages/config/locales/crowdin/th.yml +++ b/modules/storages/config/locales/crowdin/th.yml @@ -32,6 +32,10 @@ th: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/tr.yml b/modules/storages/config/locales/crowdin/tr.yml index 47244e9eb507..56a04ba12b0b 100644 --- a/modules/storages/config/locales/crowdin/tr.yml +++ b/modules/storages/config/locales/crowdin/tr.yml @@ -32,6 +32,10 @@ tr: messages: not_linked_to_project: "projeye bağlı değildir." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/uk.yml b/modules/storages/config/locales/crowdin/uk.yml index 37e3cd49c3de..5b02a57526f3 100644 --- a/modules/storages/config/locales/crowdin/uk.yml +++ b/modules/storages/config/locales/crowdin/uk.yml @@ -32,6 +32,10 @@ uk: messages: not_linked_to_project: "– не пов’язано з проєктом." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/vi.yml b/modules/storages/config/locales/crowdin/vi.yml index d032530f19e2..e9fa4a3e5506 100644 --- a/modules/storages/config/locales/crowdin/vi.yml +++ b/modules/storages/config/locales/crowdin/vi.yml @@ -32,6 +32,10 @@ vi: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/zh-CN.yml b/modules/storages/config/locales/crowdin/zh-CN.yml index bb86bb3df41f..df88f41059ba 100644 --- a/modules/storages/config/locales/crowdin/zh-CN.yml +++ b/modules/storages/config/locales/crowdin/zh-CN.yml @@ -32,6 +32,10 @@ zh-CN: messages: not_linked_to_project: "未链接到项目。" models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "对于此存储不可用。" storages/storage: attributes: host: diff --git a/modules/storages/config/locales/crowdin/zh-TW.yml b/modules/storages/config/locales/crowdin/zh-TW.yml index 593d15bb25f6..184425961a75 100644 --- a/modules/storages/config/locales/crowdin/zh-TW.yml +++ b/modules/storages/config/locales/crowdin/zh-TW.yml @@ -32,6 +32,10 @@ zh-TW: messages: not_linked_to_project: "未鏈結至專案。" models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/config/locales/en.yml b/modules/storages/config/locales/en.yml index ba44b8009b7a..a6fb2b06dc77 100644 --- a/modules/storages/config/locales/en.yml +++ b/modules/storages/config/locales/en.yml @@ -36,6 +36,10 @@ en: messages: not_linked_to_project: "is not linked to project." models: + storages/project_storage: + attributes: + project_folder_mode: + mode_unavailable: "is not available for this storage." storages/storage: attributes: host: diff --git a/modules/storages/spec/contracts/storages/project_storages/base_contract_spec.rb b/modules/storages/spec/contracts/storages/project_storages/base_contract_spec.rb index 74e98789f6b5..5408397323c2 100644 --- a/modules/storages/spec/contracts/storages/project_storages/base_contract_spec.rb +++ b/modules/storages/spec/contracts/storages/project_storages/base_contract_spec.rb @@ -33,8 +33,8 @@ include_context 'ModelContract shared context' let(:contract) { described_class.new(project_storage, build_stubbed(:admin)) } - # Creator is not writable in BaseContract; just test base contract writable attributes - let(:project_storage) { build(:project_storage) } + let(:storage) { build_stubbed(:nextcloud_storage) } + let(:project_storage) { build(:project_storage, storage:) } context 'if the project folder mode is `inactive`' do before do @@ -47,11 +47,21 @@ context 'if the project folder mode is `automatic`' do before do project_storage.project_folder_mode = 'automatic' + project_storage.storage.automatically_managed = true end it_behaves_like 'contract is valid' end + context 'when the project folder mode is `automatic` but the storage is not automatically managed' do + before do + project_storage.project_folder_mode = 'automatic' + project_storage.storage.automatically_managed = false + end + + it_behaves_like 'contract is invalid', project_folder_mode: :mode_unavailable + end + context 'if the project folder mode is `manual`' do before do project_storage.project_folder_mode = 'manual' diff --git a/modules/storages/spec/factories/storage_factory.rb b/modules/storages/spec/factories/storage_factory.rb index 72c93ac69910..95e8b92a945d 100644 --- a/modules/storages/spec/factories/storage_factory.rb +++ b/modules/storages/spec/factories/storage_factory.rb @@ -33,6 +33,10 @@ sequence(:host) { |n| "https://host#{n}.example.com" } creator factory: :user + trait :as_generic do + provider_type { 'Storages::Storage' } + end + factory :nextcloud_storage, class: '::Storages::NextcloudStorage' do provider_type { Storages::Storage::PROVIDER_TYPE_NEXTCLOUD } diff --git a/modules/storages/spec/features/manage_project_storage_spec.rb b/modules/storages/spec/features/manage_project_storage_spec.rb index ce9ff6bd5573..601abbb35149 100644 --- a/modules/storages/spec/features/manage_project_storage_spec.rb +++ b/modules/storages/spec/features/manage_project_storage_spec.rb @@ -32,9 +32,7 @@ # This tests assumes that a Storage has already been setup # in the Admin section, tested by admin_storage_spec.rb. RSpec.describe( - 'Activation of storages in projects', - js: true, - webmock: true + 'Activation of storages in projects', :js, :webmock ) do let(:user) { create(:user) } # The first page is the Project -> Settings -> General page, so we need @@ -154,7 +152,7 @@ # 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) + id: Storages::ProjectStorage.last) expect(page).to have_text('Edit the file storage to this project') expect(page).not_to have_select('storages_project_storage_storage_id') expect(page).to have_text(storage.name) @@ -173,7 +171,7 @@ # 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) + id: Storages::ProjectStorage.last) expect(page).to have_text('Edit the file storage to this project') page.click_link('Cancel') expect(page).to have_current_path project_settings_project_storages_path(project) @@ -182,8 +180,8 @@ page.find('.icon.icon-delete').click # Danger zone confirmation flow - expect(page).to have_selector('.form--section-title', text: "DELETE FILE STORAGE") - expect(page).to have_selector('.danger-zone--warning', text: "Deleting a file storage is an irreversible action.") + expect(page).to have_css('.form--section-title', text: "DELETE FILE STORAGE") + expect(page).to have_css('.danger-zone--warning', text: "Deleting a file storage is an irreversible action.") expect(page).to have_button('Delete', disabled: true) # Cancel Confirmation @@ -201,6 +199,32 @@ expect(page).to have_text(I18n.t('storages.no_results')) end + describe 'automatic project folder mode' do + context 'when the storage is not automatically managed' do + let(:oauth_application) { create(:oauth_application) } + let(:storage) { create(:nextcloud_storage, :as_not_automatically_managed, oauth_application:) } + let(:project_storage) { create(:project_storage, storage:, project:) } + + it 'automatic option is not available' do + visit edit_project_settings_project_storage_path(project_id: project, id: project_storage) + + expect(page).not_to have_content('New folder with automatically managed permissions') + end + end + + context 'when the storage is automatically managed' do + let(:oauth_application) { create(:oauth_application) } + let(:storage) { create(:nextcloud_storage, :as_automatically_managed, oauth_application:) } + let(:project_storage) { create(:project_storage, storage:, project:) } + + it 'automatic option is available' do + visit edit_project_settings_project_storage_path(project_id: project, id: project_storage) + + expect(page).to have_content('New folder with automatically managed permissions') + end + end + end + describe 'configuration checks' do let(:configured_storage) { storage } let!(:unconfigured_storage) { create(:storage) } diff --git a/modules/storages/spec/models/project_storage_spec.rb b/modules/storages/spec/models/project_storage_spec.rb index 2c4ab2ef23b7..7f61dcaa7ed9 100644 --- a/modules/storages/spec/models/project_storage_spec.rb +++ b/modules/storages/spec/models/project_storage_spec.rb @@ -78,6 +78,38 @@ end end + describe '#automatic_management_possible?' do + let(:project_storage) { build_stubbed(:project_storage, storage:) } + + context 'when the storage is not a NextcloudStorage' do + let(:storage) { build_stubbed(:storage, :as_generic) } + + it "returns false" do + expect(project_storage.automatic_management_possible?).to be false + end + end + + context 'when the storage is a NextcloudStorage' do + let(:storage) { build_stubbed(:nextcloud_storage) } + + context 'when the storage is not automatically managed' do + it "returns false" do + expect(project_storage.automatic_management_possible?).to be false + end + end + + context 'when the storage is automatically managed' do + before do + storage.automatically_managed = true + end + + it "returns true" do + expect(project_storage.automatic_management_possible?).to be true + end + end + end + end + describe '#project_folder_mode' do let(:project_storage) { build(:project_storage) } diff --git a/spec/features/work_packages/attachments/attachment_upload_spec.rb b/spec/features/work_packages/attachments/attachment_upload_spec.rb index 84275745a891..2745c41e031e 100644 --- a/spec/features/work_packages/attachments/attachment_upload_spec.rb +++ b/spec/features/work_packages/attachments/attachment_upload_spec.rb @@ -252,7 +252,8 @@ attachments.drag_and_drop_file '[data-qa-selector="op-attachments--drop-box"]', image_fixture.path, :center, - page.find('[data-qa-tab-id="files"]') + page.find('[data-qa-tab-id="files"]'), + delay_dragleave: true expect(page).to have_selector('[data-qa-selector="op-files-tab--file-list-item-title"]', text: 'image.png', wait: 10) editor.wait_until_upload_progress_toaster_cleared @@ -265,7 +266,8 @@ attachments.drag_and_drop_file '.work-package-comment', image_fixture.path, :center, - page.find('[data-qa-tab-id="activity"]') + page.find('[data-qa-tab-id="activity"]'), + delay_dragleave: true wp_page.expect_tab 'Activity' end @@ -281,12 +283,48 @@ editor.wait_until_upload_progress_toaster_cleared expect(page).to have_selector('[data-qa-selector="op-files-tab--file-list-item-title"]', text: 'image.png', wait: 5) + # Drop zone should become hidden again + expect(container).not_to be_visible + ## # and via drag & drop attachments.drag_and_drop_file(container, image_fixture.path) editor.wait_until_upload_progress_toaster_cleared expect(page) .to have_selector('[data-qa-selector="op-files-tab--file-list-item-title"]', text: 'image.png', count: 2, wait: 5) + + # Drop zone should become hidden again + expect(container).not_to be_visible + + ## + # and via drag & drop having a stopover over a ckEditor input field (Regression#49507) + attachments.drag_and_drop_file container, + image_fixture.path, + :center, + ["#{field.selector} #{field.display_selector}", '.work-package--single-view'] + + editor.wait_until_upload_progress_toaster_cleared + expect(page) + .to have_css('[data-qa-selector="op-files-tab--file-list-item-title"]', text: 'image.png', count: 3, wait: 5) + + # Drop zone should become hidden again + expect(container).not_to be_visible + + ## + # and via drag & drop having a stopover and canceling the action, should restore the drop zones + # (Regression#45782) + attachments.drag_and_drop_file container, + image_fixture.path, + :center, + field.input_element, + cancel_drop: true + + editor.wait_until_upload_progress_toaster_cleared + expect(page) + .to have_css('[data-qa-selector="op-files-tab--file-list-item-title"]', text: 'image.png', count: 3, wait: 5) + + # Drop zone should become hidden again + expect(container).not_to be_visible end end end diff --git a/spec/support/components/attachments/attachments.rb b/spec/support/components/attachments/attachments.rb index 48c5a1670f1e..c3d2ef9c3be7 100644 --- a/spec/support/components/attachments/attachments.rb +++ b/spec/support/components/attachments/attachments.rb @@ -7,12 +7,16 @@ class Attachments ## # Drag and Drop the file loaded from path on to the (native) target element - def drag_and_drop_file(target, path, position = :center, stopover = nil) + def drag_and_drop_file(target, path, position = :center, stopover = nil, cancel_drop: false, delay_dragleave: false) # Remove any previous input, if any page.execute_script <<-JS jQuery('#temporary_attachment_files').remove() JS + if stopover.is_a?(Array) && !stopover.all?(String) + raise ArgumentError, 'In case the stopover is an array, it must contain only string selectors.' + end + element = if target.is_a?(String) target @@ -27,7 +31,9 @@ def drag_and_drop_file(target, path, position = :center, stopover = nil) element, 'temporary_attachment_files', position.to_s, - stopover + stopover, + cancel_drop, + delay_dragleave ) attach_file_on_input(path, 'temporary_attachment_files') diff --git a/spec/support/components/attachments/attachments_input.js b/spec/support/components/attachments/attachments_input.js index adb52da6c1bd..cd42573992b9 100644 --- a/spec/support/components/attachments/attachments_input.js +++ b/spec/support/components/attachments/attachments_input.js @@ -10,7 +10,67 @@ let name = params[1]; let position = params[2]; // We might want to drag the file over something, then wait a bit and drag it elsewhere -let stopover = params[3]; +let stopovers; + +if (params[3] === null) { + stopovers = []; +} else if (Array.isArray(params[3])) { + stopovers = params[3]; +} else { + stopovers = [params[3]]; +} + +// Cancel the drop event +let cancelDrop = params[4]; + +// Delay drag leave to allow the work package tabs to become active on dragover event +let delayDragleave = params[5]; + +function buildDragEvent(type, targetX, targetY, dataTransfer) { + let event = new MouseEvent(type, { clientX: targetX, clientY: targetY, bubbles: true }); + + // Override the constructor to the DragEvent class + Object.setPrototypeOf(event, null); + event.dataTransfer = dataTransfer; + Object.setPrototypeOf(event, DragEvent.prototype); + return event; +} + +function dropOnStopover(stopover, dataTransfer) { + // Look up the selector + if (typeof stopover === 'string') { + stopover = document.querySelector(stopover); + } + + // We need coordinates to drop to the element + let stopbox = stopover.getBoundingClientRect(); + let stopX; + let stopY; + + stopX = stopbox.left + (stopbox.width / 2); + stopY = stopbox.top + (stopbox.height / 2); + + // Fire multiple drag events, to better simulate the mouse movement. + let eventTypes = ['dragenter', 'dragover', 'dragleave'] + + if (cancelDrop) { + // Firing a second 'dragleave' means the drag and drop is canceled. + eventTypes.push('dragleave'); + } + + eventTypes.forEach(function (type) { + let event = buildDragEvent(type, stopX, stopY, dataTransfer); + if (delayDragleave && type === 'dragleave') { + setTimeout(() => { + console.log("Dispatching event %O", event); + stopover.dispatchEvent(event); + }, 500); + } else { + console.log("Dispatching event %O", event); + stopover.dispatchEvent(event); + } + }); +} function dropOnTarget(dataTransfer) { // Look up the selector @@ -37,13 +97,7 @@ function dropOnTarget(dataTransfer) { } ['dragenter', 'dragover', 'drop'].forEach(function (type) { - let event = new MouseEvent(type, { clientX: targetX, clientY: targetY }); - - // Override the constructor to the DragEvent class - Object.setPrototypeOf(event, null); - event.dataTransfer = dataTransfer; - Object.setPrototypeOf(event, DragEvent.prototype); - + let event = buildDragEvent(type, targetX, targetY, dataTransfer); console.log("Dispatching event %O", event); target.dispatchEvent(event); }); @@ -71,31 +125,21 @@ let input = jQuery('') setDragImage : function setDragImage(){} }; - // If we have a stopover, do that first and then get the target - if (stopover) { - // We need coordinates to drop to the element - let stopbox = stopover.getBoundingClientRect(); - let stopX; - let stopY; - - stopX = stopbox.left + (stopbox.width / 2); - stopY = stopbox.top + (stopbox.height / 2); - - ['dragenter', 'dragover'].forEach(function (type) { - let event = new MouseEvent(type, { clientX: stopX, clientY: stopY }); - - // Override the constructor to the DragEvent class - Object.setPrototypeOf(event, null); - event.dataTransfer = dataTransfer; - Object.setPrototypeOf(event, DragEvent.prototype); - - console.log("Dispatching event %O", event); - stopover.dispatchEvent(event); - }); - - setTimeout(() => dropOnTarget(dataTransfer), 2000); + // If we have stopovers, do those first and then get the target + if (stopovers.length > 0) { + stopovers.forEach((stopover) => dropOnStopover(stopover, dataTransfer)); + + setTimeout(() => { + if (!cancelDrop) { + // After we left the stopover DOM elements, the target element should remain visible. + // If it's not visible, we raise an error. + if (target.offsetParent === null) { + throw new Error("Cannot drop the file on an invisible target"); + }; + dropOnTarget(dataTransfer); + } + }, 2000); } else { dropOnTarget(dataTransfer); } - });