diff --git a/app/assets/javascripts/projects/details/upload_button.vue b/app/assets/javascripts/projects/details/upload_button.vue
index d2158c6d9d4b375be671a2e02a18fdea5e8a06a0..7d037bd6fce2776778f2fc8f66c6b5c4997dfa5a 100644
--- a/app/assets/javascripts/projects/details/upload_button.vue
+++ b/app/assets/javascripts/projects/details/upload_button.vue
@@ -22,12 +22,18 @@ export default {
canPushCode: {
default: false,
},
+ canPushToBranch: {
+ default: false,
+ },
path: {
default: '',
},
projectPath: {
default: '',
},
+ emptyRepo: {
+ default: false,
+ },
},
uploadBlobModalId: UPLOAD_BLOB_MODAL_ID,
};
@@ -49,7 +55,9 @@ export default {
:target-branch="targetBranch"
:original-branch="originalBranch"
:can-push-code="canPushCode"
+ :can-push-to-branch="canPushToBranch"
:path="path"
+ :empty-repo="emptyRepo"
/>
diff --git a/app/assets/javascripts/projects/upload_file.js b/app/assets/javascripts/projects/upload_file.js
index a1b19abee6bed7e9e735fc34c99c54d501385fbb..794bf1dfd653be106fcbdf8641c84c52dc904caa 100644
--- a/app/assets/javascripts/projects/upload_file.js
+++ b/app/assets/javascripts/projects/upload_file.js
@@ -8,7 +8,7 @@ export const initUploadFileTrigger = () => {
if (!uploadFileTriggerEl) return false;
- const { targetBranch, originalBranch, canPushCode, path, projectPath } =
+ const { targetBranch, originalBranch, canPushCode, canPushToBranch, path, projectPath } =
uploadFileTriggerEl.dataset;
return new Vue({
@@ -18,8 +18,10 @@ export const initUploadFileTrigger = () => {
targetBranch,
originalBranch,
canPushCode: parseBoolean(canPushCode),
+ canPushToBranch: parseBoolean(canPushToBranch),
path,
projectPath,
+ emptyRepo: true,
},
render(h) {
return h(UploadButton);
diff --git a/app/assets/javascripts/repository/components/blob_button_group.vue b/app/assets/javascripts/repository/components/blob_button_group.vue
index 0a9df26306cc28351b3c315207d098ba052aff0f..cea445bc5cfb7d4f68f6333b82b7e1b8e50311d3 100644
--- a/app/assets/javascripts/repository/components/blob_button_group.vue
+++ b/app/assets/javascripts/repository/components/blob_button_group.vue
@@ -1,12 +1,10 @@
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/repository/components/header_area.vue b/app/assets/javascripts/repository/components/header_area.vue
index f62eea9583de875f3cd0d3f068209057bf36e54f..0d0898955275594c00ef69a65ca775704a817c2c 100644
--- a/app/assets/javascripts/repository/components/header_area.vue
+++ b/app/assets/javascripts/repository/components/header_area.vue
@@ -42,6 +42,7 @@ export default {
'canCollaborate',
'canEditTree',
'canPushCode',
+ 'canPushToBranch',
'originalBranch',
'selectedBranch',
'newBranchPath',
@@ -179,6 +180,7 @@ export default {
:can-collaborate="canCollaborate"
:can-edit-tree="canEditTree"
:can-push-code="canPushCode"
+ :can-push-to-branch="canPushToBranch"
:original-branch="originalBranch"
:selected-branch="selectedBranch"
:new-branch-path="newBranchPath"
diff --git a/app/assets/javascripts/repository/components/header_area/breadcrumbs.vue b/app/assets/javascripts/repository/components/header_area/breadcrumbs.vue
index 1f1c2f24c7aca0d4c795a67cfd6c68852591970f..2675cbc458aed216bedd691e9cc7edc5b66dccd3 100644
--- a/app/assets/javascripts/repository/components/header_area/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/header_area/breadcrumbs.vue
@@ -79,6 +79,11 @@ export default {
required: false,
default: false,
},
+ canPushToBranch: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
selectedBranch: {
type: String,
required: false,
@@ -332,6 +337,7 @@ export default {
:target-branch="selectedBranch"
:original-branch="originalBranch"
:can-push-code="canPushCode"
+ :can-push-to-branch="canPushToBranch"
:path="uploadPath"
/>
-import {
- GlModal,
- GlForm,
- GlFormGroup,
- GlFormInput,
- GlFormTextarea,
- GlButton,
- GlAlert,
- GlFormCheckbox,
-} from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
+import { logError } from '~/lib/logger';
import { contentTypeMultipartFormData } from '~/lib/utils/headers';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
-import {
- SECONDARY_OPTIONS_TEXT,
- COMMIT_LABEL,
- TARGET_BRANCH_LABEL,
- TOGGLE_CREATE_MR_LABEL,
-} from '../constants';
-
-const PRIMARY_OPTIONS_TEXT = __('Upload file');
-const MODAL_TITLE = __('Upload new file');
-const REMOVE_FILE_TEXT = __('Remove file');
-const NEW_BRANCH_IN_FORK = __(
- 'GitLab will create a branch in your fork and start a merge request.',
-);
-const ERROR_MESSAGE = __('Error uploading file. Please try again.');
+import CommitChangesModal from '~/repository/components/commit_changes_modal.vue';
export default {
components: {
- GlModal,
- GlForm,
- GlFormGroup,
- GlFormInput,
- GlFormTextarea,
GlButton,
UploadDropzone,
- GlAlert,
FileIcon,
- GlFormCheckbox,
+ CommitChangesModal,
},
i18n: {
- COMMIT_LABEL,
- TARGET_BRANCH_LABEL,
- TOGGLE_CREATE_MR_LABEL,
- REMOVE_FILE_TEXT,
- NEW_BRANCH_IN_FORK,
+ REMOVE_FILE_TEXT: __('Remove file'),
+ ERROR_MESSAGE: __('Error uploading file. Please try again.'),
},
props: {
- modalTitle: {
- type: String,
- default: MODAL_TITLE,
- required: false,
- },
- primaryBtnText: {
- type: String,
- default: PRIMARY_OPTIONS_TEXT,
- required: false,
- },
modalId: {
type: String,
required: true,
@@ -83,6 +43,10 @@ export default {
type: Boolean,
required: true,
},
+ canPushToBranch: {
+ type: Boolean,
+ required: true,
+ },
path: {
type: String,
required: true,
@@ -92,45 +56,25 @@ export default {
default: null,
required: false,
},
+ emptyRepo: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
- commit: this.commitMessage,
- target: this.targetBranch,
- createNewMr: true,
file: null,
filePreviewURL: null,
- fileBinary: null,
loading: false,
};
},
computed: {
- primaryOptions() {
- return {
- text: this.primaryBtnText,
- attributes: {
- variant: 'confirm',
- loading: this.loading,
- disabled: !this.formCompleted || this.loading,
- },
- };
- },
- cancelOptions() {
- return {
- text: SECONDARY_OPTIONS_TEXT,
- attributes: {
- disabled: this.loading,
- },
- };
- },
formattedFileSize() {
return numberToHumanSize(this.file.size);
},
- showCreateNewMrToggle() {
- return this.canPushCode && this.target !== this.originalBranch;
- },
- formCompleted() {
- return this.file && this.commit && this.target;
+ isValid() {
+ return Boolean(this.file);
},
},
methods: {
@@ -152,14 +96,18 @@ export default {
this.file = null;
this.filePreviewURL = null;
},
- submitForm() {
- return this.replacePath ? this.replaceFile() : this.uploadFile();
+ submitForm(formData) {
+ return this.replacePath ? this.replaceFile(formData) : this.uploadFile(formData);
},
- submitRequest(method, url) {
+ submitRequest(method, url, formData) {
+ this.loading = true;
+
+ formData.append('file', this.file);
+
return axios({
method,
url,
- data: this.formData(),
+ data: formData,
headers: {
...contentTypeMultipartFormData,
},
@@ -167,30 +115,23 @@ export default {
.then((response) => {
visitUrl(response.data.filePath);
})
- .catch(() => {
+ .catch((e) => {
+ logError(
+ `Failed to ${this.replacePath ? 'replace' : 'upload'} file. See exception details for more information.`,
+ e,
+ );
+ createAlert({ message: this.$options.i18n.ERROR_MESSAGE });
+ })
+ .finally(() => {
this.loading = false;
- createAlert({ message: ERROR_MESSAGE });
});
},
- formData() {
- const formData = new FormData();
- formData.append('branch_name', this.target);
- formData.append('create_merge_request', this.createNewMr);
- formData.append('commit_message', this.commit);
- formData.append('file', this.file);
-
- return formData;
- },
- replaceFile() {
- this.loading = true;
-
- // The PUT path can be geneated from $route (similar to "uploadFile") once router is connected
+ replaceFile(formData) {
+ // The PUT path can be generated from $route (similar to "uploadFile") once router is connected
// Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/332736
- return this.submitRequest('put', this.replacePath);
+ return this.submitRequest('put', this.replacePath, formData);
},
- uploadFile() {
- this.loading = true;
-
+ uploadFile(formData) {
const {
$route: {
params: { path },
@@ -198,22 +139,28 @@ export default {
} = this;
const uploadPath = joinPaths(this.path, path);
- return this.submitRequest('post', uploadPath);
+ return this.submitRequest('post', uploadPath, formData);
},
},
validFileMimetypes: [],
};
-
-
+
+
-
-
-
-
-
-
-
- {{ $options.i18n.TOGGLE_CREATE_MR_LABEL }}
-
-
- {{ $options.i18n.NEW_BRANCH_IN_FORK }}
-
-
-
+
+
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 33127a165f8bf497e48091782011d3d262d307ca..b3a368bd6e7572296ed49a7ba557d7d6287fcee4 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -223,6 +223,7 @@ export default function setupVueRepositoryList() {
canCollaborate,
canEditTree,
canPushCode,
+ canPushToBranch,
selectedBranch,
newBranchPath,
newTagPath,
@@ -249,6 +250,7 @@ export default function setupVueRepositoryList() {
currentPath: this.$route.params.path,
refType: this.$route.query.ref_type,
canCollaborate: parseBoolean(canCollaborate),
+ canPushToBranch: parseBoolean(canPushToBranch),
canEditTree: parseBoolean(canEditTree),
canPushCode: parseBoolean(canPushCode),
originalBranch: ref,
diff --git a/app/assets/javascripts/repository/init_header_app.js b/app/assets/javascripts/repository/init_header_app.js
index 9d4082b7f2554be514fb48a9116d875aa5ee352c..58f0daf97bac77885b70e895a56155d011a30540 100644
--- a/app/assets/javascripts/repository/init_header_app.js
+++ b/app/assets/javascripts/repository/init_header_app.js
@@ -40,6 +40,7 @@ export default function initHeaderApp({ router, isReadmeView = false, isBlobView
breadcrumbsCanCollaborate,
breadcrumbsCanEditTree,
breadcrumbsCanPushCode,
+ breadcrumbsCanPushToBranch,
breadcrumbsSelectedBranch,
breadcrumbsNewBranchPath,
breadcrumbsNewTagPath,
@@ -88,6 +89,7 @@ export default function initHeaderApp({ router, isReadmeView = false, isBlobView
canCollaborate: parseBoolean(breadcrumbsCanCollaborate),
canEditTree: parseBoolean(breadcrumbsCanEditTree),
canPushCode: parseBoolean(breadcrumbsCanPushCode),
+ canPushToBranch: parseBoolean(breadcrumbsCanPushToBranch),
originalBranch: ref,
selectedBranch: breadcrumbsSelectedBranch,
newBranchPath: breadcrumbsNewBranchPath,
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 52b9872b7935206c16615a17821da3f52f66a1bc..f46b5852adfe62bf42f0751b396d8e54cbc3bfc6 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -115,6 +115,7 @@ def breadcrumb_data_attributes
attrs = {
selected_branch: selected_branch,
can_push_code: can?(current_user, :push_code, @project).to_s,
+ can_push_to_branch: user_access(@project).can_push_to_branch?(@ref).to_s,
can_collaborate: can_collaborate_with_project?(@project).to_s,
new_blob_path: project_new_blob_path(@project, @ref),
upload_path: project_create_blob_path(@project, @ref),
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 59a83133f90fccc1935a2a5ec9b1bcadf01f8cb8..a89cb2e1071e4ceb6629c5d071631b5eb1b103d8 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -266,6 +266,7 @@ def upload_anchor_data
'target_branch' => default_branch_or_main,
'original_branch' => default_branch_or_main,
'can_push_code' => 'true',
+ 'can_push_to_branch' => 'true',
'path' => project_create_blob_path(project, default_branch_or_main),
'project_path' => project.full_path
}
diff --git a/ee/spec/frontend/repository/mock_data.js b/ee/spec/frontend/repository/mock_data.js
index 8740d7dcc125512d511113c3488d8d9eb85cefe3..56131b1b4ab520d900f3fb54b994a16e1b6a20f8 100644
--- a/ee/spec/frontend/repository/mock_data.js
+++ b/ee/spec/frontend/repository/mock_data.js
@@ -24,6 +24,7 @@ export const headerAppInjected = {
canCollaborate: true,
canEditTree: true,
canPushCode: true,
+ canPushToBranch: true,
originalBranch: 'main',
selectedBranch: 'feature/new-ui',
newBranchPath: '/project/new-branch',
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6cc5ed9ebc4c26e8c91639a4f39204473df787fa..d706652d1f529a5782079a44da250abdc25206ad 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -23115,6 +23115,9 @@ msgstr ""
msgid "Failed to delete custom emoji. Please try again."
msgstr ""
+msgid "Failed to delete file! Please try again."
+msgstr ""
+
msgid "Failed to deploy to"
msgstr ""
@@ -25048,6 +25051,9 @@ msgstr ""
msgid "GitLab will create a branch in your fork and start a merge request."
msgstr ""
+msgid "GitLab will create a default branch, %{branchName}, and commit your changes."
+msgstr ""
+
msgid "GitLab.com (SaaS)"
msgstr ""
@@ -46175,9 +46181,6 @@ msgstr ""
msgid "Replace all labels"
msgstr ""
-msgid "Replace file"
-msgstr ""
-
msgid "Replaced all labels with %{label_references} %{label_text}."
msgstr ""
@@ -59589,9 +59592,6 @@ msgstr ""
msgid "Upload file"
msgstr ""
-msgid "Upload new file"
-msgstr ""
-
msgid "Uploaded date"
msgstr ""
diff --git a/scripts/frontend/quarantined_vue3_specs.txt b/scripts/frontend/quarantined_vue3_specs.txt
index 14c0ef9ed3907658b8cd22b807ddab56674059df..6f8732c041b299eea5d305e2b7bf66fb70ca26a3 100644
--- a/scripts/frontend/quarantined_vue3_specs.txt
+++ b/scripts/frontend/quarantined_vue3_specs.txt
@@ -282,7 +282,6 @@ spec/frontend/releases/components/tag_create_spec.js
spec/frontend/releases/components/tag_field_exsting_spec.js
spec/frontend/releases/components/tag_search_spec.js
spec/frontend/repository/components/header_area/blob_controls_spec.js
-spec/frontend/repository/components/header_area/breadcrumbs_spec.js
spec/frontend/repository/components/table/index_spec.js
spec/frontend/repository/components/table/row_spec.js
spec/frontend/repository/router_spec.js
diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb
index 148ccd9e399021e048b0ecbdd1bb4381a579045b..926efbd4c2cb8d66db9cdf8f2c368777e8fe19d0 100644
--- a/spec/features/projects/files/user_replaces_files_spec.rb
+++ b/spec/features/projects/files/user_replaces_files_spec.rb
@@ -48,12 +48,11 @@
click_on('Replace')
find(".upload-dropzone-card").drop(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
- page.within('#modal-replace-blob') do
+ within_testid('upload-blob-modal') do
fill_in(:commit_message, with: 'Replacement file commit message')
+ click_button('Commit changes')
end
- click_button('Replace file')
-
expect(page).to have_content('Lorem ipsum dolor sit amet')
expect(page).to have_content('Sed ut perspiciatis unde omnis')
expect(page).to have_content('Replacement file commit message')
@@ -89,10 +88,9 @@
page.within('#modal-replace-blob') do
fill_in(:commit_message, with: 'Replacement file commit message')
+ click_button('Commit changes')
end
- click_button('Replace file')
-
expect(page).to have_content('Replacement file commit message')
fork = user.fork_of(project2.reload)
@@ -124,14 +122,13 @@
click_on('Replace')
epoch = Time.zone.now.strftime('%s%L').last(5)
- expect(find_field(_('Target branch')).value).to eq "#{user.username}-protected-branch-patch-#{epoch}"
+ expect(find_field(_('Commit to a new branch')).value).to eq "#{user.username}-protected-branch-patch-#{epoch}"
find(".upload-dropzone-card").drop(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
page.within('#modal-replace-blob') do
fill_in(:commit_message, with: 'Replacement file commit message')
+ click_button('Commit changes')
end
- click_button('Replace file')
-
expect(page).to have_content('Replacement file commit message')
expect(page).to have_current_path(project_new_merge_request_path(project3), ignore_query: true)
diff --git a/spec/fixtures/sample.pdf b/spec/fixtures/sample.pdf
index 81ea09d7d12267293c04bc0ad1bd938a70bf4187..c01805e89c1684e79130151abc23bb80401583a9 100644
Binary files a/spec/fixtures/sample.pdf and b/spec/fixtures/sample.pdf differ
diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js
index 89f764372cb717dda52ab9dbbab02df40e1ed0b9..d9568dd61ef6c09e286842d37b02c543899d4942 100644
--- a/spec/frontend/repository/components/blob_button_group_spec.js
+++ b/spec/frontend/repository/components/blob_button_group_spec.js
@@ -132,14 +132,12 @@ describe('BlobButtonGroup component', () => {
const title = `Replace ${name}`;
expect(findUploadBlobModal().props()).toMatchObject({
- modalTitle: title,
commitMessage: title,
targetBranch,
originalBranch,
canPushCode,
path,
replacePath,
- primaryBtnText: 'Replace file',
});
});
@@ -157,7 +155,6 @@ describe('BlobButtonGroup component', () => {
canPushCode,
emptyRepo,
isUsingLfs,
- handleFormSubmit: expect.any(Function),
});
});
});
diff --git a/spec/frontend/repository/components/commit_changes_modal_spec.js b/spec/frontend/repository/components/commit_changes_modal_spec.js
index 5b98fd51acbdb89660a8aff22c3ce69ad667a380..941ae8307c7d0a004fe0baef9aa4113662a1c001 100644
--- a/spec/frontend/repository/components/commit_changes_modal_spec.js
+++ b/spec/frontend/repository/components/commit_changes_modal_spec.js
@@ -139,6 +139,26 @@ describe('CommitChangesModal', () => {
});
expect(findSlot().text()).toBe('test form fields slot');
});
+
+ it('disables actionable while loading', () => {
+ createComponent({ props: { loading: true } });
+
+ expect(findModal().props('actionPrimary').attributes).toEqual(
+ expect.objectContaining({ disabled: true }),
+ );
+ expect(findModal().props('actionCancel').attributes).toEqual(
+ expect.objectContaining({ disabled: true }),
+ );
+ expect(findCommitTextarea().attributes()).toEqual(
+ expect.objectContaining({ disabled: 'true' }),
+ );
+ expect(findCurrentBranchRadioOption().attributes()).toEqual(
+ expect.objectContaining({ disabled: 'true' }),
+ );
+ expect(findNewBranchRadioOption().attributes()).toEqual(
+ expect.objectContaining({ disabled: 'true' }),
+ );
+ });
});
describe('form', () => {
@@ -147,6 +167,15 @@ describe('CommitChangesModal', () => {
expect(findForm().attributes('action')).toBe(initialProps.actionPath);
});
+ it('shows the correct form fields when repo is empty', () => {
+ createComponent({ props: { emptyRepo: true } });
+ expect(findCommitTextarea().exists()).toBe(true);
+ expect(findRadioGroup().exists()).toBe(false);
+ expect(findModal().text()).toContain(
+ 'GitLab will create a default branch, main, and commit your changes.',
+ );
+ });
+
it('shows the correct form fields when commit to current branch', () => {
createComponent();
expect(findCommitTextarea().exists()).toBe(true);
@@ -176,12 +205,8 @@ describe('CommitChangesModal', () => {
});
describe('when `canPushToCode` is `false`', () => {
- const commitInBranchMessage = sprintf(
- 'Your changes can be committed to %{branchName} because a merge request is open.',
- {
- branchName: 'main',
- },
- );
+ const commitInBranchMessage =
+ 'Your changes can be committed to main because a merge request is open.';
it('shows the correct form fields when `branchAllowsCollaboration` is `true`', () => {
createComponent({ props: { canPushCode: false, branchAllowsCollaboration: true } });
@@ -292,17 +317,11 @@ describe('CommitChangesModal', () => {
});
describe('form submission', () => {
- const handleFormSubmitSpy = jest.fn();
-
beforeEach(async () => {
- createFullComponent({ props: { handleFormSubmit: handleFormSubmitSpy } });
+ createFullComponent();
await nextTick();
});
- afterEach(() => {
- handleFormSubmitSpy.mockRestore();
- });
-
describe('invalid form', () => {
beforeEach(async () => {
findFormRadioGroup().vm.$emit('input', true);
@@ -319,7 +338,24 @@ describe('CommitChangesModal', () => {
it('does not submit form', () => {
findModal().vm.$emit('primary', { preventDefault: () => {} });
- expect(handleFormSubmitSpy).not.toHaveBeenCalled();
+ expect(wrapper.emitted('submit-form')).toBeUndefined();
+ });
+ });
+
+ describe('invalid prop is passed in', () => {
+ beforeEach(() => {
+ createComponent({ props: { isValid: false } });
+ });
+
+ it('disables submit button', () => {
+ expect(findModal().props('actionPrimary').attributes).toEqual(
+ expect.objectContaining({ disabled: true }),
+ );
+ });
+
+ it('does not submit form', () => {
+ findModal().vm.$emit('primary', { preventDefault: () => {} });
+ expect(wrapper.emitted('submit-form')).toBeUndefined();
});
});
@@ -339,9 +375,18 @@ describe('CommitChangesModal', () => {
);
});
- it('submits form', () => {
- findModal().vm.$emit('primary', { preventDefault: () => {} });
- expect(handleFormSubmitSpy).toHaveBeenCalled();
+ it('submits form', async () => {
+ await findModal().vm.$emit('primary', { preventDefault: jest.fn() });
+ await nextTick();
+ const submission = wrapper.emitted('submit-form')[0][0];
+ expect(Object.fromEntries(submission)).toStrictEqual({
+ authenticity_token: 'mock-csrf-token',
+ branch_name: 'some valid target branch',
+ branch_selection: 'true',
+ commit_message: 'some valid commit message',
+ create_merge_request: '1',
+ original_branch: 'main',
+ });
});
});
});
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..308c3d1674d6ffe3e2c2e2d92787f450c3b23524
--- /dev/null
+++ b/spec/frontend/repository/components/delete_blob_modal_spec.js
@@ -0,0 +1,104 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMount } from '@vue/test-utils';
+import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue';
+import CommitChangesModal from '~/repository/components/commit_changes_modal.vue';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import * as urlUtility from '~/lib/utils/url_utility';
+import { createAlert } from '~/alert';
+import { logError } from '~/lib/logger';
+
+jest.mock('~/alert');
+jest.mock('~/lib/logger');
+
+describe('DeleteBlobModal', () => {
+ let wrapper;
+ let mock;
+ let visitUrlSpy;
+
+ const initialProps = {
+ deletePath: '/delete/blob',
+ modalId: 'Delete-blob',
+ commitMessage: 'Delete File',
+ targetBranch: 'some-target-branch',
+ originalBranch: 'main',
+ canPushCode: true,
+ canPushToBranch: true,
+ emptyRepo: false,
+ isUsingLfs: false,
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMount(DeleteBlobModal, {
+ propsData: {
+ ...initialProps,
+ },
+ stubs: {
+ CommitChangesModal,
+ },
+ });
+ };
+
+ const findCommitChangesModal = () => wrapper.findComponent(CommitChangesModal);
+ const submitForm = async () => {
+ findCommitChangesModal().vm.$emit('submit-form', new FormData());
+
+ await axios.waitForAll();
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl');
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('renders commit change modal with correct props', () => {
+ expect(findCommitChangesModal().props()).toStrictEqual({
+ branchAllowsCollaboration: false,
+ canPushCode: true,
+ canPushToBranch: true,
+ commitMessage: 'Delete File',
+ emptyRepo: false,
+ isUsingLfs: false,
+ loading: false,
+ modalId: 'Delete-blob',
+ originalBranch: 'main',
+ targetBranch: 'some-target-branch',
+ valid: true,
+ });
+ });
+
+ describe('form submission', () => {
+ it('handles successful request', async () => {
+ mock.onPost(initialProps.deletePath).reply(HTTP_STATUS_OK, { filePath: 'blah' });
+
+ await submitForm();
+
+ expect(visitUrlSpy).toHaveBeenCalledWith('blah');
+ });
+
+ it('handles failed request', async () => {
+ mock = new MockAdapter(axios);
+ mock.onPost(initialProps.deletePath).timeout();
+
+ await submitForm();
+
+ const mockError = new Error('timeout of 0ms exceeded');
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Failed to delete file! Please try again.',
+ error: mockError,
+ });
+ expect(logError).toHaveBeenCalledWith(
+ 'Failed to delete file. See exception details for more information.',
+ mockError,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/header_area/breadcrumbs_spec.js b/spec/frontend/repository/components/header_area/breadcrumbs_spec.js
index f9653521b50895adccd6d2ff6a889219d0479ac8..53324051883141f22e7db966f7ec66c501831bac 100644
--- a/spec/frontend/repository/components/header_area/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/header_area/breadcrumbs_spec.js
@@ -196,9 +196,20 @@ describe('Repository breadcrumbs component', () => {
});
it('renders the modal once loaded', async () => {
- await nextTick();
+ await waitForPromises();
expect(findUploadBlobModal().exists()).toBe(true);
+ expect(findUploadBlobModal().props()).toStrictEqual({
+ canPushCode: false,
+ canPushToBranch: false,
+ commitMessage: 'Upload New File',
+ emptyRepo: false,
+ modalId: 'modal-upload-blob',
+ originalBranch: '',
+ path: '',
+ replacePath: null,
+ targetBranch: '',
+ });
});
});
@@ -211,7 +222,7 @@ describe('Repository breadcrumbs component', () => {
});
it('renders the modal once loaded', async () => {
- await nextTick();
+ await waitForPromises();
expect(findNewDirectoryModal().exists()).toBe(true);
expect(findNewDirectoryModal().props('path')).toBe('root/master/some_dir');
diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js
index c43afaac1acaff448a6ce1b7797f6531845caa5a..d8c293deed20984429894ac4aeac6d59eadec4d2 100644
--- a/spec/frontend/repository/components/upload_blob_modal_spec.js
+++ b/spec/frontend/repository/components/upload_blob_modal_spec.js
@@ -1,21 +1,23 @@
-import { GlModal, GlFormInput, GlFormTextarea, GlFormCheckbox, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
-import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import { visitUrl } from '~/lib/utils/url_utility';
+import * as urlUtility from '~/lib/utils/url_utility';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
+import CommitChangesModal from '~/repository/components/commit_changes_modal.vue';
+import { logError } from '~/lib/logger';
jest.mock('~/alert');
-jest.mock('~/lib/utils/url_utility', () => ({
- visitUrl: jest.fn(),
- joinPaths: () => '/new_upload',
-}));
+jest.mock('~/lib/logger');
+
+const NEW_PATH = '/new-upload';
+const REPLACE_PATH = '/replace-path';
+const ERROR_UPLOAD = 'Failed to upload file. See exception details for more information.';
+const ERROR_REPLACE = 'Failed to replace file. See exception details for more information.';
const initialProps = {
modalId: 'upload-blob',
@@ -23,14 +25,14 @@ const initialProps = {
targetBranch: 'main',
originalBranch: 'main',
canPushCode: true,
- path: 'new_upload',
+ canPushToBranch: true,
+ path: NEW_PATH,
};
describe('UploadBlobModal', () => {
let wrapper;
let mock;
-
- const mockEvent = { preventDefault: jest.fn() };
+ let visitUrlSpy;
const createComponent = (props) => {
wrapper = shallowMount(UploadBlobModal, {
@@ -38,6 +40,9 @@ describe('UploadBlobModal', () => {
...initialProps,
...props,
},
+ stubs: {
+ CommitChangesModal,
+ },
mocks: {
$route: {
params: {
@@ -48,209 +53,110 @@ describe('UploadBlobModal', () => {
});
};
- const findModal = () => wrapper.findComponent(GlModal);
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findCommitMessage = () => wrapper.findComponent(GlFormTextarea);
- const findBranchName = () => wrapper.findComponent(GlFormInput);
- const findMrCheckbox = () => wrapper.findComponent(GlFormCheckbox);
- const findUploadDropzone = () => wrapper.findComponent(UploadDropzone);
- const actionButtonDisabledState = () => findModal().props('actionPrimary').attributes.disabled;
- const cancelButtonDisabledState = () => findModal().props('actionCancel').attributes.disabled;
- const actionButtonLoadingState = () => findModal().props('actionPrimary').attributes.loading;
- const findFileIcon = () => wrapper.findComponent(FileIcon);
+ beforeEach(() => {
+ visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl');
+ mock = new MockAdapter(axios);
- describe.each`
- canPushCode | displayBranchName | displayForkedBranchMessage
- ${true} | ${true} | ${false}
- ${false} | ${false} | ${true}
- `(
- 'canPushCode = $canPushCode',
- ({ canPushCode, displayBranchName, displayForkedBranchMessage }) => {
- beforeEach(() => {
- createComponent({ canPushCode });
- });
+ mock.onPut(REPLACE_PATH).replyOnce(HTTP_STATUS_OK, { filePath: '/replace_file' });
+ });
- it('displays the modal', () => {
- expect(findModal().exists()).toBe(true);
- });
+ afterEach(() => {
+ mock.restore();
+ });
- it('includes the upload dropzone', () => {
- expect(findUploadDropzone().exists()).toBe(true);
- });
+ const setupUploadMock = () => {
+ mock.onPost(NEW_PATH).replyOnce(HTTP_STATUS_OK, { filePath: '/new_file' });
+ };
+ const setupUploadMockAsError = () => {
+ mock.onPost(NEW_PATH).timeout();
+ };
+ const setupReplaceMock = () => {
+ mock.onPut(REPLACE_PATH).replyOnce(HTTP_STATUS_OK, { filePath: '/replace_file' });
+ };
+ const setupReplaceMockAsError = () => {
+ mock.onPut(REPLACE_PATH).timeout();
+ };
- it('includes the commit message', () => {
- expect(findCommitMessage().exists()).toBe(true);
- });
+ const findCommitChangesModal = () => wrapper.findComponent(CommitChangesModal);
+ const findUploadDropzone = () => wrapper.findComponent(UploadDropzone);
+ const findFileIcon = () => wrapper.findComponent(FileIcon);
+ const submitForm = async () => {
+ findCommitChangesModal().vm.$emit('submit-form', new FormData());
- it('displays the disabled upload button', () => {
- expect(actionButtonDisabledState()).toBe(true);
- });
+ await axios.waitForAll();
+ };
- it('displays the enabled cancel button', () => {
- expect(cancelButtonDisabledState()).toBe(false);
- });
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- it('does not display the MR checkbox', () => {
- expect(findMrCheckbox().exists()).toBe(false);
+ it('renders commit changes modal', () => {
+ expect(findCommitChangesModal().props()).toMatchObject({
+ modalId: 'upload-blob',
+ commitMessage: 'Upload New File',
+ targetBranch: 'main',
+ originalBranch: 'main',
+ canPushCode: true,
+ canPushToBranch: true,
+ valid: false,
+ loading: false,
+ emptyRepo: false,
});
+ });
- it(`${
- displayForkedBranchMessage ? 'displays' : 'does not display'
- } the forked branch message`, () => {
- expect(findAlert().exists()).toBe(displayForkedBranchMessage);
- });
+ it('includes the upload dropzone', () => {
+ expect(findUploadDropzone().exists()).toBe(true);
+ });
+ });
- it(`${displayBranchName ? 'displays' : 'does not display'} the branch name`, () => {
- expect(findBranchName().exists()).toBe(displayBranchName);
+ describe.each`
+ props | setupMock | setupMockAsError | expectedVisitUrl | expectedError
+ ${{}} | ${setupUploadMock} | ${setupUploadMockAsError} | ${'/new_file'} | ${ERROR_UPLOAD}
+ ${{ replacePath: REPLACE_PATH }} | ${setupReplaceMock} | ${setupReplaceMockAsError} | ${'/replace_file'} | ${ERROR_REPLACE}
+ `(
+ 'with props=$props',
+ ({ props, setupMock, setupMockAsError, expectedVisitUrl, expectedError }) => {
+ beforeEach(async () => {
+ setupMock();
+ createComponent(props);
+ await nextTick();
});
- if (canPushCode) {
- describe('when changing the branch name', () => {
- it('displays the MR checkbox', async () => {
- createComponent({ targetBranch: 'Not main' });
-
- await nextTick();
-
- expect(findMrCheckbox().exists()).toBe(true);
- });
- });
- }
-
describe('completed form', () => {
beforeEach(() => {
findUploadDropzone().vm.$emit(
'change',
- new File(['http://file.com?format=jpg'], 'file.jpg'),
+ new File(['http://gitlab.com/-/uploads/file.jpg'], 'file.jpg'),
);
});
it('enables the upload button when the form is completed', () => {
- expect(actionButtonDisabledState()).toBe(false);
+ expect(findCommitChangesModal().props('valid')).toBe(true);
});
- describe('form submission', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- findModal().vm.$emit('primary', mockEvent);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('disables the upload button', () => {
- expect(actionButtonDisabledState()).toBe(true);
- });
-
- it('sets the upload button to loading', () => {
- expect(actionButtonLoadingState()).toBe(true);
- });
+ it('displays the correct file type icon', () => {
+ expect(findFileIcon().props('fileName')).toBe('file.jpg');
});
- describe('successful response', () => {
- beforeEach(async () => {
- mock = new MockAdapter(axios);
- mock.onPost(initialProps.path).reply(HTTP_STATUS_OK, { filePath: 'blah' });
-
- findModal().vm.$emit('primary', mockEvent);
-
- await waitForPromises();
- });
-
- it('displays the correct file type icon', () => {
- expect(findFileIcon().props('fileName')).toBe('file.jpg');
- });
-
- it('redirects to the uploaded file', () => {
- expect(visitUrl).toHaveBeenCalled();
- });
+ it('on submit, redirects to the uploaded file', async () => {
+ await submitForm();
- afterEach(() => {
- mock.restore();
- });
+ expect(visitUrlSpy).toHaveBeenCalledWith(expectedVisitUrl);
});
- describe('error response', () => {
- beforeEach(async () => {
- mock = new MockAdapter(axios);
- mock.onPost(initialProps.path).timeout();
-
- findModal().vm.$emit('primary', mockEvent);
+ it('on error, creates an alert error', async () => {
+ setupMockAsError();
+ await submitForm();
- await waitForPromises();
- });
-
- it('creates an alert error', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: 'Error uploading file. Please try again.',
- });
- });
+ const mockError = new Error('timeout of 0ms exceeded');
- afterEach(() => {
- mock.restore();
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Error uploading file. Please try again.',
});
+ expect(logError).toHaveBeenCalledWith(expectedError, mockError);
});
});
},
);
-
- describe('blob file submission type', () => {
- const submitRequest = async () => {
- mock = new MockAdapter(axios);
- findModal().vm.$emit('primary', mockEvent);
- await waitForPromises();
- };
-
- describe('upload blob file', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('displays the default "Upload new file" modal title', () => {
- expect(findModal().props('title')).toBe('Upload new file');
- });
-
- it('display the defaul primary button text', () => {
- expect(findModal().props('actionPrimary').text).toBe('Upload file');
- });
-
- it('makes a POST request', async () => {
- await submitRequest();
-
- expect(mock.history.put).toHaveLength(0);
- expect(mock.history.post).toHaveLength(1);
- });
- });
-
- describe('replace blob file', () => {
- const modalTitle = 'Replace foo.js';
- const replacePath = 'replace-path';
- const primaryBtnText = 'Replace file';
-
- beforeEach(() => {
- createComponent({
- modalTitle,
- replacePath,
- primaryBtnText,
- });
- });
-
- it('displays the passed modal title', () => {
- expect(findModal().props('title')).toBe(modalTitle);
- });
-
- it('display the passed primary button text', () => {
- expect(findModal().props('actionPrimary').text).toBe(primaryBtnText);
- });
-
- it('makes a PUT request', async () => {
- await submitRequest();
-
- expect(mock.history.put).toHaveLength(1);
- expect(mock.history.post).toHaveLength(0);
- expect(mock.history.put[0].url).toBe(replacePath);
- });
- });
- });
});
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index 272947fe54d63407d0ca4f300121ab78e9039dcf..e7ab1eb607b295f041b4f5ef0f9c7602e3655b15 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -191,6 +191,7 @@ export const headerAppInjected = {
canCollaborate: true,
canEditTree: true,
canPushCode: true,
+ canPushToBranch: true,
originalBranch: 'main',
selectedBranch: 'feature/new-ui',
newBranchPath: '/project/new-branch',
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index 61d1c8039ff15779c92d87c3407ae8e3d44b3158..0acf6a88198a30996da4a320d7c38bd76dfb252b 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -19,6 +19,39 @@
end
end
+ describe '#breadcrumb_data_attributes' do
+ let(:ref) { 'main' }
+ let(:base_attributes) do
+ {
+ selected_branch: ref,
+ can_push_code: 'false',
+ can_push_to_branch: 'false',
+ can_collaborate: 'false',
+ new_blob_path: project_new_blob_path(project, ref),
+ upload_path: project_create_blob_path(project, ref),
+ new_dir_path: project_create_dir_path(project, ref),
+ new_branch_path: new_project_branch_path(project),
+ new_tag_path: new_project_tag_path(project),
+ can_edit_tree: 'false'
+ }
+ end
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ helper.instance_variable_set(:@ref, ref)
+ allow(helper).to receive(:selected_branch).and_return(ref)
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:can?).and_return(false)
+ allow(helper).to receive(:user_access).and_return(instance_double(Gitlab::UserAccess, can_push_to_branch?: false))
+ allow(helper).to receive(:can_collaborate_with_project?).and_return(false)
+ allow(helper).to receive(:can_edit_tree?).and_return(false)
+ end
+
+ it 'returns a list of breadcrumb attributes' do
+ expect(helper.breadcrumb_data_attributes).to eq(base_attributes)
+ end
+ end
+
describe '#vue_file_list_data' do
it 'returns a list of attributes related to the project' do
helper.instance_variable_set(:@ref_type, 'heads')
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 652509f62dd01328866b8608978082dbc7c5eee1..9ce360fdc8000b67535db989f2a4aab21165229f 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -674,6 +674,7 @@
label: a_string_including('Upload file'),
data: {
"can_push_code" => "true",
+ "can_push_to_branch" => "true",
"original_branch" => "master",
"path" => "/#{project.full_path}/-/create/master",
"project_path" => project.full_path,
diff --git a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb
index 806ffdad2f14ce63a8464e36cfe17f58ca439497..63d77e9e0a02866eae5371ad63a33e4a72cd1c6b 100644
--- a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb
+++ b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb
@@ -18,11 +18,11 @@
page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message')
+ choose(option: true)
+ fill_in(:branch_name, with: 'upload_text', visible: true)
+ click_button('Commit changes')
end
- fill_in(:branch_name, with: 'upload_text', visible: true)
- click_button('Upload file')
-
expect(page).to have_content('New commit message')
expect(page).to have_current_path(project_new_merge_request_path(project), ignore_query: true)
@@ -54,8 +54,9 @@
page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message')
+ choose(option: true)
fill_in(:branch_name, with: 'upload_image', visible: true)
- click_button('Upload file')
+ click_button('Commit changes')
end
wait_for_all_requests
@@ -84,15 +85,19 @@
page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message')
+ choose(option: true)
fill_in(:branch_name, with: 'upload_image', visible: true)
- click_button('Upload file')
+ click_button('Commit changes')
end
wait_for_all_requests
visit(project_blob_path(project, 'upload_image/sample.pdf'))
+ wait_for_all_requests
+
expect(page).to have_css('.js-pdf-viewer')
+ expect(page).not_to have_content('An error occurred while loading the file. Please try again later.')
end
end
@@ -121,10 +126,9 @@
page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message')
+ click_button('Commit changes')
end
- click_button('Upload file')
-
expect(page).to have_content('New commit message')
fork = user.fork_of(project2.reload)
@@ -159,10 +163,9 @@
page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message')
+ click_button('Commit changes')
end
- click_button('Upload file')
-
expect(page).to have_content('New commit message')
page.within('.repo-breadcrumb') do
@@ -184,10 +187,9 @@
page.within('#details-modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message')
+ click_button('Commit changes')
end
- click_button('Upload file')
-
expect(page).to have_content('New commit message')
expect(page).to have_content('Lorem ipsum dolor sit amet')
expect(page).to have_content('Sed ut perspiciatis unde omnis')