diff --git a/.rubocop_todo/gitlab/feature_available_usage.yml b/.rubocop_todo/gitlab/feature_available_usage.yml index 2993409a8283cd3c5afef652349b6136a862b99c..38c54bf41938a35d29b726c212f79333b88b2cbe 100644 --- a/.rubocop_todo/gitlab/feature_available_usage.yml +++ b/.rubocop_todo/gitlab/feature_available_usage.yml @@ -27,6 +27,7 @@ Gitlab/FeatureAvailableUsage: - 'ee/app/helpers/ee/form_helper.rb' - 'ee/app/helpers/ee/graph_helper.rb' - 'ee/app/helpers/ee/issues_helper.rb' + - 'ee/app/helpers/ee/lock_helper.rb' - 'ee/app/helpers/ee/operations_helper.rb' - 'ee/app/helpers/ee/projects/incidents_helper.rb' - 'ee/app/helpers/ee/projects_helper.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 17fc374e1e4ef9d95f1d6864f2b4ce6eaac1a4f3..40e6ec8b7aa03a34b9ea813341527a3690fbb9ba 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -535,6 +535,7 @@ Layout/LineLength: - 'ee/app/helpers/ee/integrations_helper.rb' - 'ee/app/helpers/ee/issues_helper.rb' - 'ee/app/helpers/ee/labels_helper.rb' + - 'ee/app/helpers/ee/lock_helper.rb' - 'ee/app/helpers/ee/merge_requests_helper.rb' - 'ee/app/helpers/ee/mirror_helper.rb' - 'ee/app/helpers/ee/notes_helper.rb' @@ -4208,6 +4209,7 @@ Layout/LineLength: - 'spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb' - 'spec/views/projects/settings/operations/show.html.haml_spec.rb' - 'spec/views/projects/tags/index.html.haml_spec.rb' + - 'spec/views/projects/tree/show.html.haml_spec.rb' - 'spec/views/shared/milestones/_issuable.html.haml_spec.rb' - 'spec/views/shared/projects/_project.html.haml_spec.rb' - 'spec/views/shared/snippets/_snippet.html.haml_spec.rb' diff --git a/.rubocop_todo/lint/unused_method_argument.yml b/.rubocop_todo/lint/unused_method_argument.yml index d5302716428dc7c4444b218e47f1670011eb87d6..f7995c5ab54b5fa337fced26e31a3253836c64ce 100644 --- a/.rubocop_todo/lint/unused_method_argument.yml +++ b/.rubocop_todo/lint/unused_method_argument.yml @@ -204,6 +204,7 @@ Lint/UnusedMethodArgument: - 'ee/app/graphql/types/security_orchestration/security_policy_source_type.rb' - 'ee/app/graphql/types/vulnerability_detail_type.rb' - 'ee/app/graphql/types/vulnerability_location_type.rb' + - 'ee/app/helpers/ee/lock_helper.rb' - 'ee/app/models/boards/epic_board.rb' - 'ee/app/models/burndown.rb' - 'ee/app/models/concerns/elastic/application_versioned_search.rb' diff --git a/.rubocop_todo/rails/helper_instance_variable.yml b/.rubocop_todo/rails/helper_instance_variable.yml index a72b9e70174433f0a6a8d06c81aad88112e7849f..c4812f05ce4517ab9a3f0be3307fb012f28a3b6a 100644 --- a/.rubocop_todo/rails/helper_instance_variable.yml +++ b/.rubocop_todo/rails/helper_instance_variable.yml @@ -59,6 +59,7 @@ Rails/HelperInstanceVariable: - 'ee/app/helpers/ee/integrations_helper.rb' - 'ee/app/helpers/ee/kerberos_helper.rb' - 'ee/app/helpers/ee/labels_helper.rb' + - 'ee/app/helpers/ee/lock_helper.rb' - 'ee/app/helpers/ee/mirror_helper.rb' - 'ee/app/helpers/ee/notes_helper.rb' - 'ee/app/helpers/ee/operations_helper.rb' diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index 891abed2ca7a894c67d0a4f01edb4a19bc06f2fa..9f2d6559e8185dee5c66701cf87e76f1779ca040 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -246,6 +246,7 @@ RSpec/ContextWording: - 'ee/spec/helpers/ee/groups_helper_spec.rb' - 'ee/spec/helpers/ee/issuables_helper_spec.rb' - 'ee/spec/helpers/ee/issues_helper_spec.rb' + - 'ee/spec/helpers/ee/lock_helper_spec.rb' - 'ee/spec/helpers/ee/operations_helper_spec.rb' - 'ee/spec/helpers/ee/personal_access_tokens_helper_spec.rb' - 'ee/spec/helpers/ee/projects/security/api_fuzzing_configuration_helper_spec.rb' diff --git a/.rubocop_todo/rspec/factory_bot/avoid_create.yml b/.rubocop_todo/rspec/factory_bot/avoid_create.yml index ba9d700a16dfee0e0caeaac7dd6c6655c3e1d56e..cd51e58ef5266881f1027400c68f2058d3d971dc 100644 --- a/.rubocop_todo/rspec/factory_bot/avoid_create.yml +++ b/.rubocop_todo/rspec/factory_bot/avoid_create.yml @@ -31,6 +31,7 @@ RSpec/FactoryBot/AvoidCreate: - 'ee/spec/helpers/ee/issuables_helper_spec.rb' - 'ee/spec/helpers/ee/issues_helper_spec.rb' - 'ee/spec/helpers/ee/labels_helper_spec.rb' + - 'ee/spec/helpers/ee/lock_helper_spec.rb' - 'ee/spec/helpers/ee/namespace_user_cap_reached_alert_helper_spec.rb' - 'ee/spec/helpers/ee/namespaces_helper_spec.rb' - 'ee/spec/helpers/ee/operations_helper_spec.rb' @@ -584,6 +585,7 @@ RSpec/FactoryBot/AvoidCreate: - 'spec/views/projects/settings/merge_requests/show.html.haml_spec.rb' - 'spec/views/projects/settings/operations/show.html.haml_spec.rb' - 'spec/views/projects/tags/index.html.haml_spec.rb' + - 'spec/views/projects/tree/show.html.haml_spec.rb' - 'spec/views/search/_results.html.haml_spec.rb' - 'spec/views/shared/_label_row.html.haml_spec.rb' - 'spec/views/shared/issuable/_sidebar.html.haml_spec.rb' diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml index 48ef0e0e9d5b897e8d51a5f979e086d55ad7457f..7ceee9c6bd0f482d41c618c0ddacb42a07608c6f 100644 --- a/.rubocop_todo/rspec/feature_category.yml +++ b/.rubocop_todo/rspec/feature_category.yml @@ -363,6 +363,7 @@ RSpec/FeatureCategory: - 'ee/spec/helpers/ee/groups/settings_helper_spec.rb' - 'ee/spec/helpers/ee/hooks_helper_spec.rb' - 'ee/spec/helpers/ee/labels_helper_spec.rb' + - 'ee/spec/helpers/ee/lock_helper_spec.rb' - 'ee/spec/helpers/ee/namespace_user_cap_reached_alert_helper_spec.rb' - 'ee/spec/helpers/ee/operations_helper_spec.rb' - 'ee/spec/helpers/ee/profiles_helper_spec.rb' @@ -3965,6 +3966,7 @@ RSpec/FeatureCategory: - 'spec/views/projects/settings/integrations/edit.html.haml_spec.rb' - 'spec/views/projects/settings/operations/show.html.haml_spec.rb' - 'spec/views/projects/tags/index.html.haml_spec.rb' + - 'spec/views/projects/tree/show.html.haml_spec.rb' - 'spec/views/shared/_label_row.html.haml_spec.rb' - 'spec/views/shared/_milestones_sort_dropdown.html.haml_spec.rb' - 'spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb' diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml index 22ee999caad96f4b789fecbe53ffc21a29e0631b..75ecde8c27d8238b14588825ed0ff4cb4a84ff06 100644 --- a/.rubocop_todo/rspec/named_subject.yml +++ b/.rubocop_todo/rspec/named_subject.yml @@ -184,6 +184,7 @@ RSpec/NamedSubject: - 'ee/spec/helpers/ee/gitlab_routing_helper_spec.rb' - 'ee/spec/helpers/ee/groups/group_members_helper_spec.rb' - 'ee/spec/helpers/ee/groups_helper_spec.rb' + - 'ee/spec/helpers/ee/lock_helper_spec.rb' - 'ee/spec/helpers/ee/merge_requests_helper_spec.rb' - 'ee/spec/helpers/ee/operations_helper_spec.rb' - 'ee/spec/helpers/ee/projects/incidents_helper_spec.rb' diff --git a/.rubocop_todo/rspec/receive_messages.yml b/.rubocop_todo/rspec/receive_messages.yml index 1f635a419676180514b9c85a4c5f45cb8612bf15..581c114c0d1d6cd9af745a3904526202aaec2492 100644 --- a/.rubocop_todo/rspec/receive_messages.yml +++ b/.rubocop_todo/rspec/receive_messages.yml @@ -25,6 +25,7 @@ RSpec/ReceiveMessages: - 'ee/spec/helpers/ee/ide_helper_spec.rb' - 'ee/spec/helpers/ee/issuables_helper_spec.rb' - 'ee/spec/helpers/ee/issues_helper_spec.rb' + - 'ee/spec/helpers/ee/lock_helper_spec.rb' - 'ee/spec/helpers/ee/namespaces_helper_spec.rb' - 'ee/spec/helpers/ee/registrations_helper_spec.rb' - 'ee/spec/helpers/ee/subscribable_banner_helper_spec.rb' @@ -570,6 +571,7 @@ RSpec/ReceiveMessages: - 'spec/views/projects/settings/operations/show.html.haml_spec.rb' - 'spec/views/projects/settings/repository/_protected_branches.html.haml_spec.rb' - 'spec/views/projects/tags/index.html.haml_spec.rb' + - 'spec/views/projects/tree/show.html.haml_spec.rb' - 'spec/views/shared/_broadcast_message.html.haml_spec.rb' - 'spec/views/shared/projects/_list.html.haml_spec.rb' - 'spec/views/shared/projects/_project_card.html.haml_spec.rb' diff --git a/.rubocop_todo/rspec/subject_declaration.yml b/.rubocop_todo/rspec/subject_declaration.yml index ec46ac41a7a1f153db1afe82adfadcf84c97a309..052605f94d5cb7f5e69162bf60632f1ed15b8315 100644 --- a/.rubocop_todo/rspec/subject_declaration.yml +++ b/.rubocop_todo/rspec/subject_declaration.yml @@ -2,6 +2,7 @@ RSpec/SubjectDeclaration: Exclude: - 'ee/spec/finders/app_sec/fuzzing/coverage/corpuses_finder_spec.rb' + - 'ee/spec/helpers/ee/lock_helper_spec.rb' - 'ee/spec/helpers/nav/new_dropdown_helper_spec.rb' - 'ee/spec/lib/ee/api/helpers/notes_helpers_spec.rb' - 'ee/spec/lib/gitlab/expiring_subscription_message_spec.rb' diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 074eafce2272802b200d5dcbd4fd3b83eb766ab8..16956a51b0ea6b20a1c2b4d1bbe1e1df0807b7ef 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -72,9 +72,10 @@ const initCodeDropdown = () => { const { sshUrl, httpUrl, kerberosUrl } = codeDropdownEl.dataset; - const CodeDropdownComponent = gon.features.directoryCodeDropdownUpdates - ? CompactCodeDropdown - : CodeDropdown; + const CodeDropdownComponent = + gon.features.directoryCodeDropdownUpdates && gon.features.blobRepositoryVueHeaderApp + ? CompactCodeDropdown + : CodeDropdown; return new Vue({ el: codeDropdownEl, diff --git a/app/assets/javascripts/repository/components/header_area.vue b/app/assets/javascripts/repository/components/header_area.vue index b000dc8cf0aad1737dbf5edd1291797b3925a68e..93c8481a3b75489458aad31731cd7358af13533e 100644 --- a/app/assets/javascripts/repository/components/header_area.vue +++ b/app/assets/javascripts/repository/components/header_area.vue @@ -265,7 +265,7 @@ export default { :upload-path="uploadPath" :new-dir-path="newDirPath" /> - <!-- EE lock directory --> + <!-- EE: = render_if_exists 'projects/tree/lock_link' --> <lock-directory-button v-if="!isRoot" :project-path="projectPath" :path="currentPath" /> <gl-button v-gl-tooltip.html="findFileTooltip" diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js index 3a809c520843b0cb8c10d1e356ad7d29626446c7..7b5c975ddfe4270693d510c5fb73b0ba5f1f30f4 100644 --- a/app/assets/javascripts/repository/index.js +++ b/app/assets/javascripts/repository/index.js @@ -188,9 +188,10 @@ export default function setupVueRepositoryList() { const { sshUrl, httpUrl, kerberosUrl, xcodeUrl, directoryDownloadLinks } = codeDropdownEl.dataset; - const CodeDropdownComponent = gon.features.directoryCodeDropdownUpdates - ? CompactCodeDropdown - : CodeDropdown; + const CodeDropdownComponent = + gon.features.directoryCodeDropdownUpdates && gon.features.blobRepositoryVueHeaderApp + ? CompactCodeDropdown + : CodeDropdown; return new Vue({ el: codeDropdownEl, diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 7feaf58246289ed519aaed5aa1ad3f8963496bc4..4ef92f34ea6ed576e91ff74406d27bd4bc376b9c 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -47,6 +47,7 @@ class Projects::BlobController < Projects::ApplicationController push_frontend_feature_flag(:inline_blame, @project) push_frontend_feature_flag(:blob_overflow_menu, current_user) push_frontend_feature_flag(:filter_blob_path, current_user) + push_frontend_feature_flag(:blob_repository_vue_header_app, @project) push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks) push_frontend_feature_flag(:directory_code_dropdown_updates, current_user) end diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 8fbf44e58f9e6af2a67600c6df03e3cf10c37e21..f4a93d03fa3c422e5f5674cb28b647a05ed18c6c 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -19,6 +19,7 @@ class Projects::TreeController < Projects::ApplicationController before_action do push_frontend_feature_flag(:inline_blame, @project) + push_frontend_feature_flag(:blob_repository_vue_header_app, @project) push_frontend_feature_flag(:blob_overflow_menu, current_user) push_frontend_feature_flag(:filter_blob_path, current_user) push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 0f4784cbdd0d51691fdd1dbd6b70b2739a25dbbf..99a3e9f2725ca2cdcd0c0b95e4b65498db89a4ef 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -42,6 +42,7 @@ class ProjectsController < Projects::ApplicationController push_frontend_feature_flag(:edit_branch_rules, @project) # TODO: We need to remove the FF eventually when we rollout page_specific_styles push_frontend_feature_flag(:page_specific_styles, current_user) + push_frontend_feature_flag(:blob_repository_vue_header_app, @project) push_frontend_feature_flag(:blob_overflow_menu, current_user) push_frontend_feature_flag(:filter_blob_path, current_user) push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks) diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml index c0751180a5370ef203b2e1e0936d822db221f794..5cf20ac8931088b83eb39eccca6cad853d610a0e 100644 --- a/app/views/projects/_files.html.haml +++ b/app/views/projects/_files.html.haml @@ -8,7 +8,12 @@ - add_page_specific_style 'page_bundles/projects' #tree-holder.tree-holder.clearfix.js-per-page.gl-mt-5{ data: { blame_per_page: Gitlab::Git::BlamePagination::PAGINATION_PER_PAGE } } - #js-repository-blob-header-app{ data: vue_tree_header_app_data(project, repository, ref, pipeline) } + - if Feature.enabled?(:blob_repository_vue_header_app, project) + #js-repository-blob-header-app{ data: vue_tree_header_app_data(project, repository, ref, pipeline) } + + - else + .nav-block.gl-flex.gl-flex-col.sm:gl-flex-row.gl-items-stretch + = render 'projects/tree/tree_header', tree: @tree - if project.forked? #js-fork-info{ data: vue_fork_divergence_data(project, ref) } diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index 664b036519efd5469db75d6964472f464454ea4a..f62667206ccfc926f05059fd821f61b21431bed7 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -8,18 +8,23 @@ - if (readme = @repository.readme) && readme.rich_viewer .tree-holder.gl-mt-5 - #js-repository-blob-header-app{ data: { - project_id: @project.id, - ref: ref, - ref_type: @ref_type.to_s, - breadcrumbs: breadcrumb_data_attributes, - project_root_path: project_path(@project), - project_path: project.full_path, - compare_path: compare_path, - web_ide_button_options: web_ide_button_data.merge(fork_options).to_json, - web_ide_button_default_branch: @project.default_branch_or_main, - escaped_ref: ActionDispatch::Journey::Router::Utils.escape_path(ref) - } } + - if Feature.enabled?(:blob_repository_vue_header_app, project) + #js-repository-blob-header-app{ data: { + project_id: @project.id, + ref: ref, + ref_type: @ref_type.to_s, + breadcrumbs: breadcrumb_data_attributes, + project_root_path: project_path(@project), + project_path: project.full_path, + compare_path: compare_path, + web_ide_button_options: web_ide_button_data.merge(fork_options).to_json, + web_ide_button_default_branch: @project.default_branch_or_main, + escaped_ref: ActionDispatch::Journey::Router::Utils.escape_path(ref) + } } + + - else + .nav-block.mt-0 + = render 'projects/tree/tree_header', tree: @tree %article.file-holder.readme-holder{ id: 'readme', class: ("limited-width-container" unless fluid_layout) } .js-file-title.file-title-flex-parent diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index ee9d2fe73e1a68ce4a274c5826fb4d2a7fe92c9b..4e91e31674fd1de39d6ec47c4b55021be33f5785 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -6,7 +6,10 @@ - if blob.rich_viewer && blob.extension != 'geojson' - add_page_startup_api_call local_assigns.fetch(:viewer_url) { url_for(safe_params.merge(viewer: blob.rich_viewer.type, format: :json)) } -#js-repository-blob-header-app{ data: vue_blob_header_app_data(project, blob, ref) } +- if Feature.enabled?(:blob_repository_vue_header_app, project) + #js-repository-blob-header-app{ data: vue_blob_header_app_data(project, blob, ref) } +- else + = render "projects/blob/breadcrumb", blob: blob - if project.forked? #js-fork-info{ data: vue_fork_divergence_data(project, ref) } diff --git a/app/views/projects/buttons/_compare.html.haml b/app/views/projects/buttons/_compare.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..82b1b837fbb9fda0222f9f1722d4a0c8eb2f8963 --- /dev/null +++ b/app/views/projects/buttons/_compare.html.haml @@ -0,0 +1,8 @@ +- project = local_assigns.fetch(:project) +- ref = local_assigns.fetch(:ref, nil) +- root_ref = local_assigns.fetch(:root_ref, nil) +- unless ref.blank? || root_ref == ref + - compare_path = project_compare_index_path(project, from: root_ref, to: ref) + + = link_button_to compare_path, class: 'shortcuts-compare', rel: 'nofollow' do + = _('Compare') diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..8d353c4c4a76bde5362e43dcc992447916b764e1 --- /dev/null +++ b/app/views/projects/tree/_tree_header.html.haml @@ -0,0 +1,21 @@ +.tree-ref-container.gl-flex.gl-flex-wrap.gl-gap-2.mb-2.mb-md-0 + .tree-ref-holder.gl-max-w-26{ data: { testid: 'ref-dropdown-container' } } + #js-tree-ref-switcher{ data: { project_id: @project.id, ref_type: @ref_type.to_s, project_root_path: project_path(@project) } } + + #js-repo-breadcrumb{ data: breadcrumb_data_attributes } + +#js-blob-controls +.tree-controls + .gl-flex.gl-flex-wrap.gl-gap-3.gl-mb-3.sm:gl-mb-0 + = render_if_exists 'projects/tree/lock_link' + = render 'projects/buttons/compare', project: @project, ref: @ref, root_ref: @repository&.root_ref + + = render 'projects/find_file_link' + = render 'shared/web_ide_button', blob: nil, css_classes: 'gl-w-full sm:gl-w-auto' + + .project-code-holder.gl-hidden.sm:gl-inline-block + = render "projects/buttons/code", dropdown_class: 'dropdown-menu-right', ref: @ref + + .project-code-holder.gl-flex.gl-gap-3{ class: 'sm:!gl-hidden' } + = render 'projects/buttons/download', project: @project, ref: @ref + = render "shared/mobile_clone_panel", ref: @ref diff --git a/config/feature_flags/gitlab_com_derisk/blob_repository_vue_header_app.yml b/config/feature_flags/gitlab_com_derisk/blob_repository_vue_header_app.yml new file mode 100644 index 0000000000000000000000000000000000000000..dbe3028f77915151b7439df0beb22ca0d674c0cb --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/blob_repository_vue_header_app.yml @@ -0,0 +1,9 @@ +--- +name: blob_repository_vue_header_app +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/509327 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/176182 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/513582 +milestone: '17.9' +group: group::source code +type: gitlab_com_derisk +default_enabled: false diff --git a/ee/app/assets/javascripts/repository/index.js b/ee/app/assets/javascripts/repository/index.js index b25fd6c9a539b5cddcd957697faef41b5d69d6dd..82ce2a5231f1a4217f97f6868ee3931f41e01ad3 100644 --- a/ee/app/assets/javascripts/repository/index.js +++ b/ee/app/assets/javascripts/repository/index.js @@ -1,7 +1,45 @@ import Vue from 'vue'; +import { createAlert } from '~/alert'; +import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; +import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import initTree from '~/repository'; import CodeOwners from '../vue_shared/components/code_owners/code_owners.vue'; +const initPathLocks = (data, router) => { + if (data.pathLocksAvailable) { + const toggleBtn = document.querySelector('a.js-path-lock'); + if (!toggleBtn) return; + + toggleBtn.addEventListener('click', async (e) => { + e.preventDefault(); + + const { dataset } = e.currentTarget; + const message = + dataset.state === 'lock' + ? __('Are you sure you want to lock this directory?') + : __('Are you sure you want to unlock this directory?'); + + const confirmed = await confirmAction(message); + if (!confirmed) return; + + toggleBtn.setAttribute('disabled', 'disabled'); + + axios + .post(data.pathLocksToggle, { + path: router.currentRoute.params.path.replace(/^\//, ''), + }) + .then(() => window.location.reload()) + .catch(() => { + toggleBtn.removeAttribute('disabled'); + createAlert({ + message: __('An error occurred while initializing path locks'), + }); + }); + }); + } +}; + const initCodeOwnersApp = (router, apolloProvider, projectPath) => { const codeOwnersEl = document.querySelector('#js-code-owners'); if (!codeOwnersEl) return null; @@ -26,7 +64,10 @@ const initCodeOwnersApp = (router, apolloProvider, projectPath) => { }; export default () => { - const { router, apolloProvider, projectPath } = initTree(); + const { router, data, apolloProvider, projectPath } = initTree(); + if (!gon.features.blobRepositoryVueHeaderApp) { + initPathLocks(data, router); + } initCodeOwnersApp(router, apolloProvider, projectPath); }; diff --git a/ee/app/helpers/ee/lock_helper.rb b/ee/app/helpers/ee/lock_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..e5abdfc31453618802acea04bc48141ca8ef2e46 --- /dev/null +++ b/ee/app/helpers/ee/lock_helper.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module EE + module LockHelper + DEFAULT_CSS_CLASSES = 'path-lock js-path-lock js-hide-on-root hidden' + + def lock_file_link(project = @project, path = @path) + return unless project.feature_available?(:file_locks) + return unless current_user + + path_lock = project.find_path_lock(path, downstream: true) + + if path_lock + locker = path_lock.user.name + + if path_lock.exact?(path) + exact_lock_file_link(path_lock, locker) + elsif path_lock.upstream?(path) + upstream_lock_file_link(path_lock, locker) + elsif path_lock.downstream?(path) + downstream_lock_file_link(path_lock, locker) + end + else + _lock_link(current_user, project) + end + end + + private + + def exact_lock_file_link(path_lock, locker) + if can_unlock?(path_lock) + tooltip = path_lock.user == current_user ? '' : "Locked by #{locker}" + enabled_lock_link("Unlock", tooltip, :unlock) + else + disabled_lock_link("Unlock", "Locked by #{locker}. You do not have permission to unlock this") + end + end + + def upstream_lock_file_link(path_lock, locker) + additional_phrase = can_unlock?(path_lock) ? 'Unlock that directory in order to unlock this' : 'You do not have permission to unlock it' + disabled_lock_link("Unlock", "#{locker} has a lock on \"#{path_lock.path}\". #{additional_phrase}") + end + + def downstream_lock_file_link(path_lock, locker) + additional_phrase = can_unlock?(path_lock) ? 'Unlock this in order to proceed' : 'You do not have permission to unlock it' + disabled_lock_link("Lock", "This directory cannot be locked while #{locker} has a lock on \"#{path_lock.path}\". #{additional_phrase}") + end + + def _lock_link(user, project) + if can?(current_user, :push_code, project) + enabled_lock_link("Lock", '', :lock) + else + disabled_lock_link("Lock", "You do not have permission to lock this") + end + end + + def disabled_lock_link(label, title) + # Disabled buttons with tooltips should have the tooltip attached + # to a wrapper element https://bootstrap-vue.org/docs/components/tooltip#disabled-elements + button = render Pajamas::ButtonComponent.new(disabled: true, button_options: { class: DEFAULT_CSS_CLASSES, data: { testid: 'disabled-lock-button' } }) do + label + end + content_tag(:span, button, title: title, class: 'btn-group has-tooltip') + end + + def enabled_lock_link(label, title, state) + render Pajamas::ButtonComponent.new(href: '#', button_options: { class: "#{DEFAULT_CSS_CLASSES} has-tooltip", title: title, data: { state: state, toggle: 'tooltip', testid: 'lock-button' } }) do + label + end + end + end +end diff --git a/ee/app/views/projects/tree/_lock_link.html.haml b/ee/app/views/projects/tree/_lock_link.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..fe03d7db3675fb0fb547bf0c1ec040198fd23825 --- /dev/null +++ b/ee/app/views/projects/tree/_lock_link.html.haml @@ -0,0 +1 @@ += lock_file_link diff --git a/ee/spec/helpers/ee/lock_helper_spec.rb b/ee/spec/helpers/ee/lock_helper_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..89dcb59dec9a91bebdfb427ded383df69563194c --- /dev/null +++ b/ee/spec/helpers/ee/lock_helper_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe EE::LockHelper do + describe '#lock_file_link' do + let!(:path_lock) { create :path_lock, path: 'app/models' } + let(:path) { path_lock.path } + let(:user) { path_lock.user } + let(:project) { path_lock.project } + let_it_be(:disabled_attr) { 'disabled="disabled"' } + + before do + allow(helper).to receive(:can?).and_return(true) + allow(helper).to receive(:current_user).and_return(user) + allow(project).to receive(:feature_available?).with(:file_locks).and_return(true) + + project.reload + end + + context 'when there is no lock' do + let(:subject) { helper.lock_file_link(project, '.gitignore') } + let_it_be(:tooltip_text) { "You do not have permission to lock this" } + + context 'when user can push code to the project' do + it 'returns an enabled "Lock" button without a tooltip' do + expect(subject).to match('Lock') + expect(subject).not_to match(disabled_attr) + expect(subject).not_to match(tooltip_text) + end + end + + context 'when user cannot push code to the project' do + before do + allow(helper).to receive(:can?).and_return(false) + end + + it 'returns a disabled "Lock" button with a tooltip' do + expect(subject).to match('Lock') + expect(subject).to match(disabled_attr) + expect(subject).to match(tooltip_text) + end + end + end + + context 'when there is no conflicting lock' do + let(:subject) { helper.lock_file_link(project, path) } + let_it_be(:tooltip_text) { "Locked by" } + + context 'when user is allowed to unlock the path' do + context 'when path was locked by the current user' do + it 'returns an enabled "Unlock" button without a tooltip' do + expect(subject).to match('Unlock') + expect(subject).not_to match(disabled_attr) + expect(subject).not_to match(tooltip_text) + end + end + + context 'wnen path was locked by someone else' do + let(:user2) { create :user } + + before do + allow(helper).to receive(:current_user).and_return(user2) + end + + it 'returns an enabled "Unlock" button with a tooltip' do + expect(subject).to match('Unlock') + expect(subject).not_to match(disabled_attr) + expect(subject).to match(tooltip_text) + end + end + end + + context 'when user is not allowed to unlock the path' do + before do + allow(helper).to receive(:can?).and_return(false) + end + + it 'returns a disabled "Unlock" button with a tooltip' do + expect(subject).to match('Unlock') + expect(subject).to match(disabled_attr) + expect(subject).to match(tooltip_text) + end + end + end + + context 'when there is an upstream lock' do + let(:requested_path) { 'app/models/user.rb' } + let(:subject) { helper.lock_file_link(project, requested_path) } + + context 'when user is allowed to unlock the upstream path' do + it 'returns a disabled "Unlock" button with a tooltip' do + expect(subject).to match('Unlock') + expect(subject).to match(disabled_attr) + expect(subject).to match("Unlock that directory in order to unlock this") + end + end + + context 'when user is not allowed to unlock the upstream path' do + before do + allow(helper).to receive(:can?).and_return(false) + end + + it 'returns a disabled "Unlock" button with a tooltip' do + expect(subject).to match('Unlock') + expect(subject).to match(disabled_attr) + expect(subject).to match("You do not have permission to unlock it") + end + end + end + + context 'when there is a downstream lock' do + let(:subject) { helper.lock_file_link(project, 'app') } + + context 'when user is allowed to unlock the downstream path' do + it 'returns a disabled "Lock" button with a tooltip' do + expect(subject).to match('Lock') + expect(subject).to match(disabled_attr) + expect(subject).to match("Unlock this in order to proceed") + end + end + + context 'when user is not allowed to unlock the downstream path' do + before do + allow(helper).to receive(:can?).and_return(false) + end + + it 'returns a disabled "Lock" button with a tooltip' do + expect(subject).to match('Lock') + expect(subject).to match(disabled_attr) + expect(subject).to match("You do not have permission to unlock it") + end + end + end + end +end diff --git a/spec/frontend/repository/components/header_area_spec.js b/spec/frontend/repository/components/header_area_spec.js index 2f959786fc559f44b6b994cd05b4384781388da8..6f68c509cc34f3e78d264236fd5a489686afedce 100644 --- a/spec/frontend/repository/components/header_area_spec.js +++ b/spec/frontend/repository/components/header_area_spec.js @@ -80,15 +80,8 @@ describe('HeaderArea', () => { expect(wrapper.exists()).toBe(true); }); - describe('Ref selector', () => { - it('renders correctly', () => { - expect(findRefSelector().exists()).toBe(true); - }); - - it('renders correctly when branch names ending with .json', () => { - createComponent({ props: { refSelectorValue: 'ends-with.json' } }); - expect(findRefSelector().exists()).toBe(true); - }); + it('renders RefSelector', () => { + expect(findRefSelector().exists()).toBe(true); }); it('renders Breadcrumbs component', () => { diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index d6fef67dc9c82a8679dc70610cf1dca5e9831aac..06b2553da71342f8063a2e21e8ae24e673908bed 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -665,6 +665,7 @@ - './ee/spec/helpers/ee/issuables_helper_spec.rb' - './ee/spec/helpers/ee/issues_helper_spec.rb' - './ee/spec/helpers/ee/labels_helper_spec.rb' +- './ee/spec/helpers/ee/lock_helper_spec.rb' - './ee/spec/helpers/ee/namespaces_helper_spec.rb' - './ee/spec/helpers/ee/namespace_user_cap_reached_alert_helper_spec.rb' - './ee/spec/helpers/ee/operations_helper_spec.rb' diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4e77173d31183a703b2566ef63d739f8ab1bfce0 --- /dev/null +++ b/spec/views/projects/tree/show.html.haml_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'projects/tree/show' do + include Devise::Test::ControllerHelpers + + let_it_be(:project) { create(:project, :repository, create_branch: 'bar') } + let(:repository) { project.repository } + let(:ref) { 'master' } + let(:commit) { repository.commit(ref) } + let(:path) { '' } + let(:tree) { repository.tree(commit.id, path) } + + before do + stub_feature_flags(blob_repository_vue_header_app: false) + assign(:project, project) + assign(:repository, repository) + + allow(view).to receive(:can?).and_return(true) + allow(view).to receive(:can_collaborate_with_project?).and_return(true) + allow(view).to receive_message_chain('user_access.can_push_to_branch?').and_return(true) + allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings) + allow(view).to receive(:current_user).and_return(project.creator) + + assign(:id, File.join(ref, path)) + assign(:ref, ref) + assign(:path, path) + assign(:last_commit, commit) + assign(:tree, tree) + end + + context 'for branch names ending on .json' do + let(:ref) { 'ends-with.json' } + + it 'displays correctly' do + render + + expect(rendered).to have_css('#js-tree-ref-switcher') + end + end + + context 'when on root ref' do + let(:ref) { repository.root_ref } + + it 'hides compare button' do + render + + expect(rendered).not_to include('Compare') + end + end + + context 'when not on root ref' do + let(:ref) { 'bar' } + + it 'shows a compare button' do + render + + expect(rendered).to include('Compare') + end + end +end