diff --git a/app/assets/javascripts/repository/components/blob_button_group.vue b/app/assets/javascripts/repository/components/blob_button_group.vue
index 424dc4529ffacdbbf77322ef5c85a736e64b877f..273825b996a3d66d896ab27504d14a64ac5b5a2c 100644
--- a/app/assets/javascripts/repository/components/blob_button_group.vue
+++ b/app/assets/javascripts/repository/components/blob_button_group.vue
@@ -3,6 +3,7 @@ import { GlButtonGroup, GlButton, GlModalDirective } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { sprintf, __ } from '~/locale';
import getRefMixin from '../mixins/get_ref';
+import DeleteBlobModal from './delete_blob_modal.vue';
import UploadBlobModal from './upload_blob_modal.vue';
export default {
@@ -15,6 +16,7 @@ export default {
GlButtonGroup,
GlButton,
UploadBlobModal,
+ DeleteBlobModal,
},
directives: {
GlModal: GlModalDirective,
@@ -41,10 +43,18 @@ export default {
type: String,
required: true,
},
+ deletePath: {
+ type: String,
+ required: true,
+ },
canPushCode: {
type: Boolean,
required: true,
},
+ emptyRepo: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
replaceModalId() {
@@ -53,6 +63,12 @@ export default {
replaceModalTitle() {
return sprintf(__('Replace %{name}'), { name: this.name });
},
+ deleteModalId() {
+ return uniqueId('delete-modal');
+ },
+ deleteModalTitle() {
+ return sprintf(__('Delete %{name}'), { name: this.name });
+ },
},
};
@@ -63,7 +79,9 @@ export default {
{{ $options.i18n.replace }}
- {{ $options.i18n.delete }}
+
+ {{ $options.i18n.delete }}
+
+
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index c3876a77ec4ccf318c0d75bcfab5f551294a2e50..09ac60c94c7e190753dc2199a4a7cfe4d38756e0 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -69,6 +69,7 @@ export default {
pushCode: false,
},
repository: {
+ empty: true,
blobs: {
nodes: [
{
@@ -92,6 +93,7 @@ export default {
forkPath: '',
simpleViewer: {},
richViewer: null,
+ webPath: '',
},
],
},
@@ -174,7 +176,9 @@ export default {
:path="path"
:name="blobInfo.name"
:replace-path="blobInfo.replacePath"
+ :delete-path="blobInfo.webPath"
:can-push-code="project.userPermissions.pushCode"
+ :empty-repo="project.repository.empty"
/>
diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue
new file mode 100644
index 0000000000000000000000000000000000000000..6599d99d7bdae9675f76e8b7fc6f600e658a0ead
--- /dev/null
+++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index 22349261d3cfff37109bd3fc99dfcb182aab2dd6..2d2faa8d9f397aa19b76faa6f109c38385e8f230 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -1,3 +1,10 @@
+import { __ } from '~/locale';
+
export const TREE_PAGE_LIMIT = 1000; // the maximum amount of items per page
export const TREE_PAGE_SIZE = 100; // the amount of items to be fetched per (batch) request
export const TREE_INITIAL_FETCH_COUNT = TREE_PAGE_LIMIT / TREE_PAGE_SIZE; // the amount of (batch) requests to make
+
+export const SECONDARY_OPTIONS_TEXT = __('Cancel');
+export const COMMIT_LABEL = __('Commit message');
+export const TARGET_BRANCH_LABEL = __('Target branch');
+export const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these changes');
diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql
index 1889f2269f593844f2abc6ea9fa0653c6b4582eb..a8f263941e29712f8ac6c61108013b9a12465f9c 100644
--- a/app/assets/javascripts/repository/queries/blob_info.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql
@@ -4,6 +4,7 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) {
pushCode
}
repository {
+ empty
blobs(paths: [$filePath]) {
nodes {
webPath
diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js
index b9a11dd1270270ba564042ee11bf5e73dc62d73b..a449fd6f06c9342b17259cedd55cd256e9fb9d0a 100644
--- a/spec/frontend/repository/components/blob_button_group_spec.js
+++ b/spec/frontend/repository/components/blob_button_group_spec.js
@@ -1,8 +1,8 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
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';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
const DEFAULT_PROPS = {
@@ -10,6 +10,8 @@ const DEFAULT_PROPS = {
path: 'some/path',
canPushCode: true,
replacePath: 'some/replace/path',
+ deletePath: 'some/delete/path',
+ emptyRepo: false,
};
const DEFAULT_INJECT = {
@@ -39,6 +41,7 @@ describe('BlobButtonGroup component', () => {
wrapper.destroy();
});
+ const findDeleteBlobModal = () => wrapper.findComponent(DeleteBlobModal);
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
const findReplaceButton = () => wrapper.findAll(GlButton).at(0);
@@ -93,4 +96,22 @@ describe('BlobButtonGroup component', () => {
primaryBtnText: 'Replace file',
});
});
+
+ it('renders DeleteBlobModel', () => {
+ createComponent();
+
+ const { targetBranch, originalBranch } = DEFAULT_INJECT;
+ const { name, canPushCode, deletePath, emptyRepo } = DEFAULT_PROPS;
+ const title = `Delete ${name}`;
+
+ expect(findDeleteBlobModal().props()).toMatchObject({
+ modalTitle: title,
+ commitMessage: title,
+ targetBranch,
+ originalBranch,
+ canPushCode,
+ deletePath,
+ emptyRepo,
+ });
+ });
});
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index e1ac46171bb10671fc19ba52c4d7b7de3dde27ad..a83d0a607f2f411f7c67c8b54207b6f768d12150 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -58,23 +58,36 @@ const richMockData = {
renderError: null,
},
};
-const userPermissionsMockData = {
+
+const projectMockData = {
userPermissions: {
pushCode: true,
},
+ repository: {
+ empty: false,
+ },
};
const localVue = createLocalVue();
const mockAxios = new MockAdapter(axios);
-const createComponentWithApollo = (mockData, mockPermissionData = true) => {
+const createComponentWithApollo = (mockData = {}) => {
localVue.use(VueApollo);
+ const defaultPushCode = projectMockData.userPermissions.pushCode;
+ const defaultEmptyRepo = projectMockData.repository.empty;
+ const { blobs, emptyRepo = defaultEmptyRepo, canPushCode = defaultPushCode } = mockData;
+
const mockResolver = jest.fn().mockResolvedValue({
data: {
project: {
- userPermissions: { pushCode: mockPermissionData },
- repository: { blobs: { nodes: [mockData] } },
+ userPermissions: { pushCode: canPushCode },
+ repository: {
+ empty: emptyRepo,
+ blobs: {
+ nodes: [blobs],
+ },
+ },
},
},
});
@@ -209,14 +222,14 @@ describe('Blob content viewer component', () => {
describe('legacy viewers', () => {
it('does not load a legacy viewer when a rich viewer is not available', async () => {
- createComponentWithApollo(simpleMockData);
+ createComponentWithApollo({ blobs: simpleMockData });
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(0);
});
it('loads a legacy viewer when a rich viewer is available', async () => {
- createComponentWithApollo(richMockData);
+ createComponentWithApollo({ blobs: richMockData });
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(1);
@@ -320,16 +333,20 @@ describe('Blob content viewer component', () => {
});
describe('BlobButtonGroup', () => {
- const { name, path, replacePath } = simpleMockData;
+ const { name, path, replacePath, webPath } = simpleMockData;
const {
userPermissions: { pushCode },
- } = userPermissionsMockData;
+ repository: { empty },
+ } = projectMockData;
it('renders component', async () => {
window.gon.current_user_id = 1;
fullFactory({
- mockData: { blobInfo: simpleMockData, project: userPermissionsMockData },
+ mockData: {
+ blobInfo: simpleMockData,
+ project: { userPermissions: { pushCode }, repository: { empty } },
+ },
stubs: {
BlobContent: true,
BlobButtonGroup: true,
@@ -342,7 +359,9 @@ describe('Blob content viewer component', () => {
name,
path,
replacePath,
+ deletePath: webPath,
canPushCode: pushCode,
+ emptyRepo: empty,
});
});
diff --git a/spec/frontend/repository/components/delete_blob_modal_spec.js b/spec/frontend/repository/components/delete_blob_modal_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..a74e3e6d325064b147ca4552505324e1afea0829
--- /dev/null
+++ b/spec/frontend/repository/components/delete_blob_modal_spec.js
@@ -0,0 +1,130 @@
+import { GlFormTextarea, GlModal, GlFormInput, GlToggle } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
+
+const initialProps = {
+ modalId: 'Delete-blob',
+ modalTitle: 'Delete File',
+ deletePath: 'some/path',
+ commitMessage: 'Delete File',
+ targetBranch: 'some-target-branch',
+ originalBranch: 'main',
+ canPushCode: true,
+ emptyRepo: false,
+};
+
+describe('DeleteBlobModal', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(DeleteBlobModal, {
+ propsData: {
+ ...initialProps,
+ ...props,
+ },
+ });
+ };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findForm = () => wrapper.findComponent({ ref: 'form' });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders Modal component', () => {
+ createComponent();
+
+ const { modalTitle: title } = initialProps;
+
+ expect(findModal().props()).toMatchObject({
+ title,
+ size: 'md',
+ actionPrimary: {
+ text: 'Delete file',
+ },
+ actionCancel: {
+ text: 'Cancel',
+ },
+ });
+ });
+
+ describe('form', () => {
+ it('gets passed the path for action attribute', () => {
+ createComponent();
+ expect(findForm().attributes('action')).toBe(initialProps.deletePath);
+ });
+
+ it('submits the form', async () => {
+ createComponent();
+
+ const submitSpy = jest.spyOn(findForm().element, 'submit');
+ findModal().vm.$emit('primary', { preventDefault: () => {} });
+ await nextTick();
+
+ expect(submitSpy).toHaveBeenCalled();
+ submitSpy.mockRestore();
+ });
+
+ it.each`
+ component | defaultValue | canPushCode | targetBranch | originalBranch | exist
+ ${GlFormTextarea} | ${initialProps.commitMessage} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${GlFormInput} | ${initialProps.targetBranch} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${GlFormInput} | ${undefined} | ${false} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${false}
+ ${GlToggle} | ${'true'} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${GlToggle} | ${undefined} | ${true} | ${'same-branch'} | ${'same-branch'} | ${false}
+ `(
+ 'has the correct form fields ',
+ ({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => {
+ createComponent({
+ canPushCode,
+ targetBranch,
+ originalBranch,
+ });
+ const formField = wrapper.findComponent(component);
+
+ if (!exist) {
+ expect(formField.exists()).toBe(false);
+ return;
+ }
+
+ expect(formField.exists()).toBe(true);
+ expect(formField.attributes('value')).toBe(defaultValue);
+ },
+ );
+
+ it.each`
+ input | value | emptyRepo | canPushCode | exist
+ ${'authenticity_token'} | ${'mock-csrf-token'} | ${false} | ${true} | ${true}
+ ${'authenticity_token'} | ${'mock-csrf-token'} | ${true} | ${false} | ${true}
+ ${'_method'} | ${'delete'} | ${false} | ${true} | ${true}
+ ${'_method'} | ${'delete'} | ${true} | ${false} | ${true}
+ ${'original_branch'} | ${initialProps.originalBranch} | ${false} | ${true} | ${true}
+ ${'original_branch'} | ${undefined} | ${true} | ${true} | ${false}
+ ${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${true}
+ ${'create_merge_request'} | ${'1'} | ${false} | ${true} | ${true}
+ ${'create_merge_request'} | ${undefined} | ${true} | ${false} | ${false}
+ `(
+ 'passes $input as a hidden input with the correct value',
+ ({ input, value, emptyRepo, canPushCode, exist }) => {
+ createComponent({
+ emptyRepo,
+ canPushCode,
+ });
+
+ const inputMethod = findForm().find(`input[name="${input}"]`);
+
+ if (!exist) {
+ expect(inputMethod.exists()).toBe(false);
+ return;
+ }
+
+ expect(inputMethod.attributes('type')).toBe('hidden');
+ expect(inputMethod.attributes('value')).toBe(value);
+ },
+ );
+ });
+});