diff --git a/app/assets/javascripts/repository/components/blob_button_group.vue b/app/assets/javascripts/repository/components/blob_button_group.vue index 273825b996a..4e7ca7b17e4 100644 --- a/app/assets/javascripts/repository/components/blob_button_group.vue +++ b/app/assets/javascripts/repository/components/blob_button_group.vue @@ -2,6 +2,7 @@ import { GlButtonGroup, GlButton, GlModalDirective } from '@gitlab/ui'; import { uniqueId } from 'lodash'; import { sprintf, __ } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import getRefMixin from '../mixins/get_ref'; import DeleteBlobModal from './delete_blob_modal.vue'; import UploadBlobModal from './upload_blob_modal.vue'; @@ -17,11 +18,12 @@ export default { GlButton, UploadBlobModal, DeleteBlobModal, + LockButton: () => import('ee_component/repository/components/lock_button.vue'), }, directives: { GlModal: GlModalDirective, }, - mixins: [getRefMixin], + mixins: [getRefMixin, glFeatureFlagMixin()], inject: { targetBranch: { default: '', @@ -55,6 +57,18 @@ export default { type: Boolean, required: true, }, + projectPath: { + type: String, + required: true, + }, + isLocked: { + type: Boolean, + required: true, + }, + canLock: { + type: Boolean, + required: true, + }, }, computed: { replaceModalId() { @@ -76,10 +90,19 @@ export default { diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 82c18d13a6a..fa358a75cc1 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -170,6 +170,7 @@ export default { this.apolloQuery(blobInfoQuery, { projectPath: this.projectPath, filePath: this.path, + ref: this.ref, }); }, apolloQuery(query, variables) { diff --git a/app/assets/javascripts/repository/mutations/lock_path.mutation.graphql b/app/assets/javascripts/repository/mutations/lock_path.mutation.graphql index 68ff22566e3..eaebc4ddf17 100644 --- a/app/assets/javascripts/repository/mutations/lock_path.mutation.graphql +++ b/app/assets/javascripts/repository/mutations/lock_path.mutation.graphql @@ -1,6 +1,7 @@ mutation toggleLock($projectPath: ID!, $filePath: String!, $lock: Boolean!) { projectSetLocked(input: { projectPath: $projectPath, filePath: $filePath, lock: $lock }) { project { + id pathLocks { nodes { path diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql index 4dba6869194..45f07f7dc58 100644 --- a/app/assets/javascripts/repository/queries/blob_info.query.graphql +++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql @@ -1,7 +1,14 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) { project(fullPath: $projectPath) { + id userPermissions { pushCode + downloadCode + } + pathLocks { + nodes { + path + } } repository { empty diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 08066acb45c..acf6b6116b8 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -44,6 +44,7 @@ class Projects::BlobController < Projects::ApplicationController before_action do push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml) push_frontend_feature_flag(:consolidated_edit_button, @project, default_enabled: :yaml) + push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks) end def new diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2cf5e98c41e..b1c09dee78d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4323,6 +4323,9 @@ msgstr "" msgid "Are you sure that you want to unarchive this project?" msgstr "" +msgid "Are you sure you want to %{action} %{name}?" +msgstr "" + msgid "Are you sure you want to cancel editing this comment?" msgstr "" diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js index a449fd6f06c..f2e54653333 100644 --- a/spec/frontend/repository/components/blob_button_group_spec.js +++ b/spec/frontend/repository/components/blob_button_group_spec.js @@ -1,5 +1,6 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import LockButton from 'ee_component/repository/components/lock_button.vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import BlobButtonGroup from '~/repository/components/blob_button_group.vue'; import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue'; @@ -12,9 +13,13 @@ const DEFAULT_PROPS = { replacePath: 'some/replace/path', deletePath: 'some/delete/path', emptyRepo: false, + projectPath: 'some/project/path', + isLocked: false, + canLock: true, }; const DEFAULT_INJECT = { + glFeatures: { fileLocks: true }, targetBranch: 'master', originalBranch: 'master', }; @@ -43,7 +48,8 @@ describe('BlobButtonGroup component', () => { const findDeleteBlobModal = () => wrapper.findComponent(DeleteBlobModal); const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal); - const findReplaceButton = () => wrapper.findAll(GlButton).at(0); + const findReplaceButton = () => wrapper.find('[data-testid="replace"]'); + const findLockButton = () => wrapper.findComponent(LockButton); it('renders component', () => { createComponent(); @@ -61,6 +67,18 @@ describe('BlobButtonGroup component', () => { createComponent(); }); + it('renders the lock button', () => { + expect(findLockButton().exists()).toBe(true); + + expect(findLockButton().props()).toMatchObject({ + canLock: true, + isLocked: false, + name: 'some name', + path: 'some/path', + projectPath: 'some/project/path', + }); + }); + it('renders both the replace and delete button', () => { expect(wrapper.findAll(GlButton)).toHaveLength(2); }); diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js index e8d6e866248..d462995328b 100644 --- a/spec/frontend/repository/components/blob_content_viewer_spec.js +++ b/spec/frontend/repository/components/blob_content_viewer_spec.js @@ -39,9 +39,6 @@ const simpleMockData = { externalStorageUrl: 'some_file.js', replacePath: 'some_file.js/replace', deletePath: 'some_file.js/delete', - canLock: true, - isLocked: false, - lockLink: 'some_file.js/lock', forkPath: 'some_file.js/fork', simpleViewer: { fileType: 'text', @@ -64,6 +61,7 @@ const richMockData = { const projectMockData = { userPermissions: { pushCode: true, + downloadCode: true, }, repository: { empty: false, @@ -77,13 +75,24 @@ const createComponentWithApollo = (mockData = {}, inject = {}) => { localVue.use(VueApollo); const defaultPushCode = projectMockData.userPermissions.pushCode; + const defaultDownloadCode = projectMockData.userPermissions.downloadCode; const defaultEmptyRepo = projectMockData.repository.empty; - const { blobs, emptyRepo = defaultEmptyRepo, canPushCode = defaultPushCode } = mockData; + const { + blobs, + emptyRepo = defaultEmptyRepo, + canPushCode = defaultPushCode, + canDownloadCode = defaultDownloadCode, + pathLocks = [], + } = mockData; mockResolver = jest.fn().mockResolvedValue({ data: { project: { - userPermissions: { pushCode: canPushCode }, + id: '1234', + userPermissions: { pushCode: canPushCode, downloadCode: canDownloadCode }, + pathLocks: { + nodes: pathLocks, + }, repository: { empty: emptyRepo, blobs: { @@ -371,7 +380,7 @@ describe('Blob content viewer component', () => { describe('BlobButtonGroup', () => { const { name, path, replacePath, webPath } = simpleMockData; const { - userPermissions: { pushCode }, + userPermissions: { pushCode, downloadCode }, repository: { empty }, } = projectMockData; @@ -381,7 +390,7 @@ describe('Blob content viewer component', () => { fullFactory({ mockData: { blobInfo: simpleMockData, - project: { userPermissions: { pushCode }, repository: { empty } }, + project: { userPermissions: { pushCode, downloadCode }, repository: { empty } }, }, stubs: { BlobContent: true, @@ -397,10 +406,37 @@ describe('Blob content viewer component', () => { replacePath, deletePath: webPath, canPushCode: pushCode, + canLock: true, + isLocked: false, emptyRepo: empty, }); }); + it.each` + canPushCode | canDownloadCode | canLock + ${true} | ${true} | ${true} + ${false} | ${true} | ${false} + ${true} | ${false} | ${false} + `('passes the correct lock states', async ({ canPushCode, canDownloadCode, canLock }) => { + fullFactory({ + mockData: { + blobInfo: simpleMockData, + project: { + userPermissions: { pushCode: canPushCode, downloadCode: canDownloadCode }, + repository: { empty }, + }, + }, + stubs: { + BlobContent: true, + BlobButtonGroup: true, + }, + }); + + await nextTick(); + + expect(findBlobButtonGroup().props('canLock')).toBe(canLock); + }); + it('does not render if not logged in', async () => { window.gon.current_user_id = null;