Commit f425cba0 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents c30b07ff 8df8a48d
<script>
import {
GlIcon,
GlLink,
GlForm,
GlFormInputGroup,
GlInputGroupText,
GlFormInput,
GlFormGroup,
GlFormTextarea,
GlButton,
GlFormRadio,
GlFormRadioGroup,
GlFormSelect,
} from '@gitlab/ui';
import { buildApiUrl } from '~/api/api_utils';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import csrf from '~/lib/utils/csrf';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
const PRIVATE_VISIBILITY = 'private';
const INTERNAL_VISIBILITY = 'internal';
const PUBLIC_VISIBILITY = 'public';
const ALLOWED_VISIBILITY = {
private: [PRIVATE_VISIBILITY],
internal: [INTERNAL_VISIBILITY, PRIVATE_VISIBILITY],
public: [INTERNAL_VISIBILITY, PRIVATE_VISIBILITY, PUBLIC_VISIBILITY],
};
export default {
components: {
GlForm,
GlIcon,
GlLink,
GlButton,
GlFormInputGroup,
GlInputGroupText,
GlFormInput,
GlFormTextarea,
GlFormGroup,
GlFormRadio,
GlFormRadioGroup,
GlFormSelect,
},
props: {
endpoint: {
type: String,
required: true,
},
newGroupPath: {
type: String,
required: true,
},
projectFullPath: {
type: String,
required: true,
},
visibilityHelpPath: {
type: String,
required: true,
},
projectId: {
type: String,
required: true,
},
projectName: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
projectDescription: {
type: String,
required: true,
},
projectVisibility: {
type: String,
required: true,
},
},
data() {
return {
isSaving: false,
namespaces: [],
selectedNamespace: {},
fork: {
name: this.projectName,
slug: this.projectPath,
description: this.projectDescription,
visibility: this.projectVisibility,
},
};
},
computed: {
projectUrl() {
return `${gon.gitlab_url}/`;
},
projectAllowedVisibility() {
return ALLOWED_VISIBILITY[this.projectVisibility];
},
namespaceAllowedVisibility() {
return (
ALLOWED_VISIBILITY[this.selectedNamespace.visibility] ||
ALLOWED_VISIBILITY[PUBLIC_VISIBILITY]
);
},
visibilityLevels() {
return [
{
text: s__('ForkProject|Private'),
value: PRIVATE_VISIBILITY,
icon: 'lock',
help: s__('ForkProject|The project can be accessed without any authentication.'),
disabled: this.isVisibilityLevelDisabled(PRIVATE_VISIBILITY),
},
{
text: s__('ForkProject|Internal'),
value: INTERNAL_VISIBILITY,
icon: 'shield',
help: s__('ForkProject|The project can be accessed by any logged in user.'),
disabled: this.isVisibilityLevelDisabled(INTERNAL_VISIBILITY),
},
{
text: s__('ForkProject|Public'),
value: PUBLIC_VISIBILITY,
icon: 'earth',
help: s__(
'ForkProject|Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.',
),
disabled: this.isVisibilityLevelDisabled(PUBLIC_VISIBILITY),
},
];
},
},
watch: {
selectedNamespace(newVal) {
const { visibility } = newVal;
if (this.projectAllowedVisibility.includes(visibility)) {
this.fork.visibility = visibility;
}
},
},
mounted() {
this.fetchNamespaces();
},
methods: {
async fetchNamespaces() {
const { data } = await axios.get(this.endpoint);
this.namespaces = data.namespaces;
},
isVisibilityLevelDisabled(visibilityLevel) {
return !(
this.projectAllowedVisibility.includes(visibilityLevel) &&
this.namespaceAllowedVisibility.includes(visibilityLevel)
);
},
async onSubmit() {
this.isSaving = true;
const { projectId } = this;
const { name, slug, description, visibility } = this.fork;
const { id: namespaceId } = this.selectedNamespace;
const postParams = {
id: projectId,
name,
namespace_id: namespaceId,
path: slug,
description,
visibility,
};
const forkProjectPath = `/api/:version/projects/:id/fork`;
const url = buildApiUrl(forkProjectPath).replace(':id', encodeURIComponent(this.projectId));
try {
const { data } = await axios.post(url, postParams);
redirectTo(data.web_url);
return;
} catch (error) {
createFlash({ message: error });
}
},
},
csrf,
};
</script>
<template>
<gl-form method="POST" @submit.prevent="onSubmit">
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
<gl-form-group label="Project name" label-for="fork-name">
<gl-form-input id="fork-name" v-model="fork.name" data-testid="fork-name-input" required />
</gl-form-group>
<div class="gl-display-flex">
<div class="gl-w-half">
<gl-form-group label="Project URL" label-for="fork-url" class="gl-pr-2">
<gl-form-input-group>
<template #prepend>
<gl-input-group-text>
{{ projectUrl }}
</gl-input-group-text>
</template>
<gl-form-select
id="fork-url"
v-model="selectedNamespace"
data-testid="fork-url-input"
required
>
<template slot="first">
<option :value="null" disabled>{{ s__('ForkProject|Select a namespace') }}</option>
</template>
<option v-for="namespace in namespaces" :key="namespace.id" :value="namespace">
{{ namespace.name }}
</option>
</gl-form-select>
</gl-form-input-group>
</gl-form-group>
</div>
<div class="gl-w-half">
<gl-form-group label="Project slug" label-for="fork-slug" class="gl-pl-2">
<gl-form-input
id="fork-slug"
v-model="fork.slug"
data-testid="fork-slug-input"
required
/>
</gl-form-group>
</div>
</div>
<p class="gl-mt-n5 gl-text-gray-500">
{{ s__('ForkProject|Want to house several dependent projects under the same namespace?') }}
<gl-link :href="newGroupPath" target="_blank">
{{ s__('ForkProject|Create a group') }}
</gl-link>
</p>
<gl-form-group label="Project description (optional)" label-for="fork-description">
<gl-form-textarea
id="fork-description"
v-model="fork.description"
data-testid="fork-description-textarea"
/>
</gl-form-group>
<gl-form-group>
<label>
{{ s__('ForkProject|Visibility level') }}
<gl-link :href="visibilityHelpPath" target="_blank">
<gl-icon name="question-o" />
</gl-link>
</label>
<gl-form-radio-group
v-model="fork.visibility"
data-testid="fork-visibility-radio-group"
required
>
<gl-form-radio
v-for="{ text, value, icon, help, disabled } in visibilityLevels"
:key="value"
:value="value"
:disabled="disabled"
:data-testid="`radio-${value}`"
>
<div>
<gl-icon :name="icon" />
<span>{{ text }}</span>
</div>
<template #help>{{ help }}</template>
</gl-form-radio>
</gl-form-radio-group>
</gl-form-group>
<div class="gl-display-flex gl-justify-content-space-between gl-mt-8">
<gl-button
type="submit"
category="primary"
variant="confirm"
data-testid="submit-button"
:loading="isSaving"
>
{{ s__('ForkProject|Fork project') }}
</gl-button>
<gl-button
type="reset"
class="gl-mr-3"
data-testid="cancel-button"
:disabled="isSaving"
:href="projectFullPath"
>
{{ s__('ForkProject|Cancel') }}
</gl-button>
</div>
</gl-form>
</template>
import Vue from 'vue'; import Vue from 'vue';
import ForkForm from './components/fork_form.vue';
import ForkGroupsList from './components/fork_groups_list.vue'; import ForkGroupsList from './components/fork_groups_list.vue';
const mountElement = document.getElementById('fork-groups-mount-element'); const mountElement = document.getElementById('fork-groups-mount-element');
const { endpoint } = mountElement.dataset;
// eslint-disable-next-line no-new if (gon.features.forkProjectForm) {
new Vue({ const {
endpoint,
newGroupPath,
projectFullPath,
visibilityHelpPath,
projectId,
projectName,
projectPath,
projectDescription,
projectVisibility,
} = mountElement.dataset;
// eslint-disable-next-line no-new
new Vue({
el: mountElement,
render(h) {
return h(ForkForm, {
props: {
endpoint,
newGroupPath,
projectFullPath,
visibilityHelpPath,
projectId,
projectName,
projectPath,
projectDescription,
projectVisibility,
},
});
},
});
} else {
const { endpoint } = mountElement.dataset;
// eslint-disable-next-line no-new
new Vue({
el: mountElement, el: mountElement,
render(h) { render(h) {
return h(ForkGroupsList, { return h(ForkGroupsList, {
...@@ -13,4 +49,5 @@ new Vue({ ...@@ -13,4 +49,5 @@ new Vue({
}, },
}); });
}, },
}); });
}
...@@ -5,7 +5,6 @@ import { ...@@ -5,7 +5,6 @@ import {
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlDropdownItem, GlDropdownItem,
GlIcon, GlIcon,
GlModalDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import permissionsQuery from 'shared_queries/repository/permissions.query.graphql'; import permissionsQuery from 'shared_queries/repository/permissions.query.graphql';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
...@@ -13,15 +12,12 @@ import { __ } from '../../locale'; ...@@ -13,15 +12,12 @@ import { __ } from '../../locale';
import getRefMixin from '../mixins/get_ref'; import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql'; import projectPathQuery from '../queries/project_path.query.graphql';
import projectShortPathQuery from '../queries/project_short_path.query.graphql'; import projectShortPathQuery from '../queries/project_short_path.query.graphql';
import UploadBlobModal from './upload_blob_modal.vue';
const ROW_TYPES = { const ROW_TYPES = {
header: 'header', header: 'header',
divider: 'divider', divider: 'divider',
}; };
const UPLOAD_BLOB_MODAL_ID = 'modal-upload-blob';
export default { export default {
components: { components: {
GlDropdown, GlDropdown,
...@@ -29,7 +25,6 @@ export default { ...@@ -29,7 +25,6 @@ export default {
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlDropdownItem, GlDropdownItem,
GlIcon, GlIcon,
UploadBlobModal,
}, },
apollo: { apollo: {
projectShortPath: { projectShortPath: {
...@@ -51,9 +46,6 @@ export default { ...@@ -51,9 +46,6 @@ export default {
}, },
}, },
}, },
directives: {
GlModal: GlModalDirective,
},
mixins: [getRefMixin], mixins: [getRefMixin],
props: { props: {
currentPath: { currentPath: {
...@@ -71,21 +63,6 @@ export default { ...@@ -71,21 +63,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
canPushCode: {
type: Boolean,
required: false,
default: false,
},
selectedBranch: {
type: String,
required: false,
default: '',
},
origionalBranch: {
type: String,
required: false,
default: '',
},
newBranchPath: { newBranchPath: {
type: String, type: String,
required: false, required: false,
...@@ -116,13 +93,7 @@ export default { ...@@ -116,13 +93,7 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
uploadPath: {
type: String,
required: false,
default: '',
},
}, },
uploadBlobModalId: UPLOAD_BLOB_MODAL_ID,
data() { data() {
return { return {
projectShortPath: '', projectShortPath: '',
...@@ -155,10 +126,7 @@ export default { ...@@ -155,10 +126,7 @@ export default {
); );
}, },
canCreateMrFromFork() { canCreateMrFromFork() {
return this.userPermissions?.forkProject && this.userPermissions?.createMergeRequestIn; return this.userPermissions.forkProject && this.userPermissions.createMergeRequestIn;
},
showUploadModal() {
return this.canEditTree && !this.$apollo.queries.userPermissions.loading;
}, },
dropdownItems() { dropdownItems() {
const items = []; const items = [];
...@@ -181,9 +149,10 @@ export default { ...@@ -181,9 +149,10 @@ export default {
{ {
attrs: { attrs: {
href: '#modal-upload-blob', href: '#modal-upload-blob',
'data-target': '#modal-upload-blob',
'data-toggle': 'modal',
}, },
text: __('Upload file'), text: __('Upload file'),
modalId: UPLOAD_BLOB_MODAL_ID,
}, },
{ {
attrs: { attrs: {
...@@ -284,26 +253,12 @@ export default { ...@@ -284,26 +253,12 @@ export default {
<gl-icon name="chevron-down" :size="16" class="float-left" /> <gl-icon name="chevron-down" :size="16" class="float-left" />
</template> </template>
<template v-for="(item, i) in dropdownItems"> <template v-for="(item, i) in dropdownItems">
<component <component :is="getComponent(item.type)" :key="i" v-bind="item.attrs">
:is="getComponent(item.type)"
:key="i"
v-bind="item.attrs"
v-gl-modal="item.modalId || null"
>
{{ item.text }} {{ item.text }}
</component> </component>
</template> </template>
</gl-dropdown> </gl-dropdown>
</li> </li>
</ol> </ol>
<upload-blob-modal
v-if="showUploadModal"
:modal-id="$options.uploadBlobModalId"
:commit-message="__('Upload New File')"
:target-branch="selectedBranch"
:origional-branch="origionalBranch"
:can-push-code="canPushCode"
:path="uploadPath"
/>
</nav> </nav>
</template> </template>
<script>
import {
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlToggle,
GlButton,
GlAlert,
} from '@gitlab/ui';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
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';
const PRIMARY_OPTIONS_TEXT = __('Upload file');
const SECONDARY_OPTIONS_TEXT = __('Cancel');
const MODAL_TITLE = __('Upload New File');
const COMMIT_LABEL = __('Commit message');
const TARGET_BRANCH_LABEL = __('Target branch');
const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these changes');
const REMOVE_FILE_TEXT = __('Remove file');
const NEW_BRANCH_IN_FORK = __(
'A new branch will be created in your fork and a new merge request will be started.',
);
const ERROR_MESSAGE = __('Error uploading file. Please try again.');
export default {
components: {
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlToggle,
GlButton,
UploadDropzone,
GlAlert,
},
i18n: {
MODAL_TITLE,
COMMIT_LABEL,
TARGET_BRANCH_LABEL,
TOGGLE_CREATE_MR_LABEL,
REMOVE_FILE_TEXT,
NEW_BRANCH_IN_FORK,
},
props: {
modalId: {
type: String,
required: true,
},
commitMessage: {
type: String,
required: true,
},
targetBranch: {
type: String,
required: true,
},
origionalBranch: {
type: String,
required: true,
},
canPushCode: {
type: Boolean,
required: true,
},
path: {
type: String,
required: true,
},
},
data() {
return {
commit: this.commitMessage,
target: this.targetBranch,
createNewMr: true,
file: null,
filePreviewURL: null,
fileBinary: null,
loading: false,
};
},
computed: {
primaryOptions() {
return {
text: PRIMARY_OPTIONS_TEXT,
attributes: [
{
variant: 'success',
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.origionalBranch;
},
formCompleted() {
return this.file && this.commit && this.target;
},
},
methods: {
setFile(file) {
this.file = file;
const fileUurlReader = new FileReader();
fileUurlReader.readAsDataURL(this.file);
fileUurlReader.onload = (e) => {
this.filePreviewURL = e.target?.result;
};
},
removeFile() {
this.file = null;
this.filePreviewURL = null;
},
uploadFile() {
this.loading = true;
const {
$route: {
params: { path },
},
} = this;
const uploadPath = joinPaths(this.path, path);
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 axios
.post(uploadPath, formData, {
headers: {
...ContentTypeMultipartFormData,
},
})
.then((response) => {
visitUrl(response.data.filePath);
})
.catch(() => {
this.loading = false;
createFlash(ERROR_MESSAGE);
});
},
},
};
</script>
<template>
<gl-form>
<gl-modal
:modal-id="modalId"
:title="$options.i18n.MODAL_TITLE"
:action-primary="primaryOptions"
:action-cancel="cancelOptions"
@primary.prevent="uploadFile"
>
<upload-dropzone class="gl-h-200! gl-mb-4" single-file-selection @change="setFile">
<div
v-if="file"
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
>
<img v-if="filePreviewURL" :src="filePreviewURL" class="gl-h-11" />
<div>{{ formattedFileSize }}</div>
<div>{{ file.name }}</div>
<gl-button
category="tertiary"
variant="confirm"
:disabled="loading"
@click="removeFile"
>{{ $options.i18n.REMOVE_FILE_TEXT }}</gl-button
>
</div>
</upload-dropzone>
<gl-form-group :label="$options.i18n.COMMIT_LABEL" label-for="commit_message">
<gl-form-textarea v-model="commit" name="commit_message" :disabled="loading" />
</gl-form-group>
<gl-form-group
v-if="canPushCode"
:label="$options.i18n.TARGET_BRANCH_LABEL"
label-for="branch_name"
>
<gl-form-input v-model="target" :disabled="loading" name="branch_name" />
</gl-form-group>
<gl-toggle
v-if="showCreateNewMrToggle"
v-model="createNewMr"
:disabled="loading"
:label="$options.i18n.TOGGLE_CREATE_MR_LABEL"
/>
<gl-alert v-if="!canPushCode" variant="info" :dismissible="false" class="gl-mt-3">
{{ $options.i18n.NEW_BRANCH_IN_FORK }}
</gl-alert>
</gl-modal>
</gl-form>
</template>
import Vue from 'vue'; import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link'; import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import { parseBoolean } from '../lib/utils/common_utils';
import { escapeFileUrl } from '../lib/utils/url_utility';
import { __ } from '../locale';
import App from './components/app.vue'; import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue'; import Breadcrumbs from './components/breadcrumbs.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue'; import DirectoryDownloadLinks from './components/directory_download_links.vue';
...@@ -55,8 +55,6 @@ export default function setupVueRepositoryList() { ...@@ -55,8 +55,6 @@ export default function setupVueRepositoryList() {
const { const {
canCollaborate, canCollaborate,
canEditTree, canEditTree,
canPushCode,
selectedBranch,
newBranchPath, newBranchPath,
newTagPath, newTagPath,
newBlobPath, newBlobPath,
...@@ -67,7 +65,8 @@ export default function setupVueRepositoryList() { ...@@ -67,7 +65,8 @@ export default function setupVueRepositoryList() {
newDirPath, newDirPath,
} = breadcrumbEl.dataset; } = breadcrumbEl.dataset;
router.afterEach(({ params: { path } }) => { router.afterEach(({ params: { path = '/' } }) => {
updateFormAction('.js-upload-blob-form', uploadPath, path);
updateFormAction('.js-create-dir-form', newDirPath, path); updateFormAction('.js-create-dir-form', newDirPath, path);
}); });
...@@ -82,16 +81,12 @@ export default function setupVueRepositoryList() { ...@@ -82,16 +81,12 @@ export default function setupVueRepositoryList() {
currentPath: this.$route.params.path, currentPath: this.$route.params.path,
canCollaborate: parseBoolean(canCollaborate), canCollaborate: parseBoolean(canCollaborate),
canEditTree: parseBoolean(canEditTree), canEditTree: parseBoolean(canEditTree),
canPushCode: parseBoolean(canPushCode),
origionalBranch: ref,
selectedBranch,
newBranchPath, newBranchPath,
newTagPath, newTagPath,
newBlobPath, newBlobPath,
forkNewBlobPath, forkNewBlobPath,
forkNewDirectoryPath, forkNewDirectoryPath,
forkUploadBlobPath, forkUploadBlobPath,
uploadPath,
}, },
}); });
}, },
......
...@@ -37,6 +37,10 @@ ...@@ -37,6 +37,10 @@
} }
} }
[data-page$='epic_boards:show'] .filter-form {
display: none;
}
.boards-app { .boards-app {
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
transition: width $sidebar-transition-duration; transition: width $sidebar-transition-duration;
......
...@@ -16,6 +16,10 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -16,6 +16,10 @@ class Projects::ForksController < Projects::ApplicationController
feature_category :source_code_management feature_category :source_code_management
before_action do
push_frontend_feature_flag(:fork_project_form)
end
def index def index
@total_forks_count = project.forks.size @total_forks_count = project.forks.size
@public_forks_count = project.forks.public_only.size @public_forks_count = project.forks.public_only.size
......
...@@ -131,8 +131,6 @@ module TreeHelper ...@@ -131,8 +131,6 @@ module TreeHelper
def breadcrumb_data_attributes def breadcrumb_data_attributes
attrs = { attrs = {
selected_branch: selected_branch,
can_push_code: can?(current_user, :push_code, @project).to_s,
can_collaborate: can_collaborate_with_project?(@project).to_s, can_collaborate: can_collaborate_with_project?(@project).to_s,
new_blob_path: project_new_blob_path(@project, @ref), new_blob_path: project_new_blob_path(@project, @ref),
upload_path: project_create_blob_path(@project, @ref), upload_path: project_create_blob_path(@project, @ref),
......
...@@ -21,4 +21,5 @@ ...@@ -21,4 +21,5 @@
#js-tree-list{ data: vue_file_list_data(project, ref) } #js-tree-list{ data: vue_file_list_data(project, ref) }
- if can_edit_tree? - if can_edit_tree?
= render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post
= render 'projects/blob/new_dir' = render 'projects/blob/new_dir'
...@@ -9,10 +9,20 @@ ...@@ -9,10 +9,20 @@
%br %br
= _('Forking a repository allows you to make changes without affecting the original project.') = _('Forking a repository allows you to make changes without affecting the original project.')
.col-lg-9 .col-lg-9
- if Feature.enabled?(:fork_project_form)
#fork-groups-mount-element{ data: { endpoint: new_project_fork_path(@project, format: :json),
new_group_path: new_group_path,
project_full_path: project_path(@project),
visibility_help_path: help_page_path("public_access/public_access"),
project_id: @project.id,
project_name: @project.name,
project_path: @project.path,
project_description: @project.description,
project_visibility: @project.visibility } }
- else
- if @own_namespace.present? - if @own_namespace.present?
.fork-thumbnail-container.js-fork-content .fork-thumbnail-container.js-fork-content
%h5.gl-mt-0.gl-mb-0.gl-ml-3.gl-mr-3 %h5.gl-mt-0.gl-mb-0.gl-ml-3.gl-mr-3
= _("Select a namespace to fork the project") = _("Select a namespace to fork the project")
= render 'fork_button', namespace: @own_namespace = render 'fork_button', namespace: @own_namespace
#fork-groups-mount-element{ data: { endpoint: new_project_fork_path(@project, format: :json) } } #fork-groups-mount-element{ data: { endpoint: new_project_fork_path(@project, format: :json) } }
---
title: Migrate bootstrap modal to GlModal for repo single file uploads
merge_request: 53623
author:
type: changed
---
name: fork_project_form
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53544
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321387
milestone: '13.10'
type: development
group: group::source code
default_enabled: false
--- ---
name: registrations_group_invite name: registrations_group_invite
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52371 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52371
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/219544 rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/351
milestone: '13.9' milestone: '13.10'
type: experiment type: experiment
group: group::expansion group: group::expansion
default_enabled: false default_enabled: false
...@@ -12036,9 +12036,6 @@ msgstr "" ...@@ -12036,9 +12036,6 @@ msgstr ""
msgid "Error uploading file" msgid "Error uploading file"
msgstr "" msgstr ""
msgid "Error uploading file. Please try again."
msgstr ""
msgid "Error uploading file: %{stripped}" msgid "Error uploading file: %{stripped}"
msgstr "" msgstr ""
...@@ -13262,6 +13259,42 @@ msgstr "" ...@@ -13262,6 +13259,42 @@ msgstr ""
msgid "Fork project?" msgid "Fork project?"
msgstr "" msgstr ""
msgid "ForkProject|Cancel"
msgstr ""
msgid "ForkProject|Create a group"
msgstr ""
msgid "ForkProject|Fork project"
msgstr ""
msgid "ForkProject|Internal"
msgstr ""
msgid "ForkProject|Private"
msgstr ""
msgid "ForkProject|Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group."
msgstr ""
msgid "ForkProject|Public"
msgstr ""
msgid "ForkProject|Select a namespace"
msgstr ""
msgid "ForkProject|The project can be accessed by any logged in user."
msgstr ""
msgid "ForkProject|The project can be accessed without any authentication."
msgstr ""
msgid "ForkProject|Visibility level"
msgstr ""
msgid "ForkProject|Want to house several dependent projects under the same namespace?"
msgstr ""
msgid "ForkedFromProjectPath|Forked from" msgid "ForkedFromProjectPath|Forked from"
msgstr "" msgstr ""
...@@ -24967,9 +25000,6 @@ msgstr "" ...@@ -24967,9 +25000,6 @@ msgstr ""
msgid "Remove due date" msgid "Remove due date"
msgstr "" msgstr ""
msgid "Remove file"
msgstr ""
msgid "Remove fork relationship" msgid "Remove fork relationship"
msgstr "" msgstr ""
...@@ -28333,9 +28363,6 @@ msgstr "" ...@@ -28333,9 +28363,6 @@ msgstr ""
msgid "Start a new merge request" msgid "Start a new merge request"
msgstr "" msgstr ""
msgid "Start a new merge request with these changes"
msgstr ""
msgid "Start a review" msgid "Start a review"
msgstr "" msgstr ""
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Projects > Files > User uploads files' do RSpec.describe 'Projects > Files > User uploads files' do
include DropzoneHelper
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :repository, name: 'Shop', creator: user) } let(:project) { create(:project, :repository, name: 'Shop', creator: user) }
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') } let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
...@@ -15,15 +17,36 @@ RSpec.describe 'Projects > Files > User uploads files' do ...@@ -15,15 +17,36 @@ RSpec.describe 'Projects > Files > User uploads files' do
context 'when a user has write access' do context 'when a user has write access' do
before do before do
visit(project_tree_path(project)) visit(project_tree_path(project))
wait_for_requests
end end
include_examples 'it uploads and commit a new text file' include_examples 'it uploads and commit a new text file'
include_examples 'it uploads and commit a new image file' include_examples 'it uploads and commit a new image file'
include_examples 'it uploads a file to a sub-directory' it 'uploads a file to a sub-directory', :js do
click_link 'files'
page.within('.repo-breadcrumb') do
expect(page).to have_content('files')
end
find('.add-to-tree').click
click_link('Upload file')
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message')
end
click_button('Upload file')
expect(page).to have_content('New commit message')
page.within('.repo-breadcrumb') do
expect(page).to have_content('files')
expect(page).to have_content('doc_sample.txt')
end
end
end end
context 'when a user does not have write access' do context 'when a user does not have write access' do
......
...@@ -9,6 +9,7 @@ RSpec.describe 'Project fork' do ...@@ -9,6 +9,7 @@ RSpec.describe 'Project fork' do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
before do before do
stub_feature_flags(fork_project_form: false)
sign_in(user) sign_in(user)
end end
......
...@@ -17,15 +17,11 @@ RSpec.describe 'Projects > Show > User uploads files' do ...@@ -17,15 +17,11 @@ RSpec.describe 'Projects > Show > User uploads files' do
context 'when a user has write access' do context 'when a user has write access' do
before do before do
visit(project_path(project)) visit(project_path(project))
wait_for_requests
end end
include_examples 'it uploads and commit a new text file' include_examples 'it uploads and commit a new text file'
include_examples 'it uploads and commit a new image file' include_examples 'it uploads and commit a new image file'
include_examples 'it uploads a file to a sub-directory'
end end
context 'when a user does not have write access' do context 'when a user does not have write access' do
......
import { GlForm, GlFormInputGroup } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
import createFlash from '~/flash';
import httpStatus from '~/lib/utils/http_status';
import * as urlUtility from '~/lib/utils/url_utility';
import ForkForm from '~/pages/projects/forks/new/components/fork_form.vue';
jest.mock('~/flash');
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
describe('ForkForm component', () => {
let wrapper;
let axiosMock;
const GON_GITLAB_URL = 'https://gitlab.com';
const GON_API_VERSION = 'v7';
const MOCK_NAMESPACES_RESPONSE = [
{
name: 'one',
id: 1,
},
{
name: 'two',
id: 2,
},
];
const DEFAULT_PROPS = {
endpoint: '/some/project-full-path/-/forks/new.json',
newGroupPath: 'some/groups/path',
projectFullPath: '/some/project-full-path',
visibilityHelpPath: 'some/visibility/help/path',
projectId: '10',
projectName: 'Project Name',
projectPath: 'project-name',
projectDescription: 'some project description',
projectVisibility: 'private',
};
const mockGetRequest = (data = {}, statusCode = httpStatus.OK) => {
axiosMock.onGet(DEFAULT_PROPS.endpoint).replyOnce(statusCode, data);
};
const createComponent = (props = {}, data = {}) => {
wrapper = shallowMount(ForkForm, {
propsData: {
...DEFAULT_PROPS,
...props,
},
data() {
return {
...data,
};
},
stubs: {
GlFormInputGroup,
},
});
};
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
window.gon = {
gitlab_url: GON_GITLAB_URL,
api_version: GON_API_VERSION,
};
});
afterEach(() => {
wrapper.destroy();
axiosMock.restore();
});
const findPrivateRadio = () => wrapper.find('[data-testid="radio-private"]');
const findInternalRadio = () => wrapper.find('[data-testid="radio-internal"]');
const findPublicRadio = () => wrapper.find('[data-testid="radio-public"]');
const findForkNameInput = () => wrapper.find('[data-testid="fork-name-input"]');
const findForkUrlInput = () => wrapper.find('[data-testid="fork-url-input"]');
const findForkSlugInput = () => wrapper.find('[data-testid="fork-slug-input"]');
const findForkDescriptionTextarea = () =>
wrapper.find('[data-testid="fork-description-textarea"]');
const findVisibilityRadioGroup = () =>
wrapper.find('[data-testid="fork-visibility-radio-group"]');
it('will go to projectFullPath when click cancel button', () => {
mockGetRequest();
createComponent();
const { projectFullPath } = DEFAULT_PROPS;
const cancelButton = wrapper.find('[data-testid="cancel-button"]');
expect(cancelButton.attributes('href')).toBe(projectFullPath);
});
it('make POST request with project param', async () => {
jest.spyOn(axios, 'post');
const namespaceId = 20;
mockGetRequest();
createComponent(
{},
{
selectedNamespace: {
id: namespaceId,
},
},
);
wrapper.find(GlForm).vm.$emit('submit', { preventDefault: () => {} });
const {
projectId,
projectDescription,
projectName,
projectPath,
projectVisibility,
} = DEFAULT_PROPS;
const url = `/api/${GON_API_VERSION}/projects/${projectId}/fork`;
const project = {
description: projectDescription,
id: projectId,
name: projectName,
namespace_id: namespaceId,
path: projectPath,
visibility: projectVisibility,
};
expect(axios.post).toHaveBeenCalledWith(url, project);
});
it('has input with csrf token', () => {
mockGetRequest();
createComponent();
expect(wrapper.find('input[name="authenticity_token"]').attributes('value')).toBe(
'mock-csrf-token',
);
});
it('pre-populate form from project props', () => {
mockGetRequest();
createComponent();
expect(findForkNameInput().attributes('value')).toBe(DEFAULT_PROPS.projectName);
expect(findForkSlugInput().attributes('value')).toBe(DEFAULT_PROPS.projectPath);
expect(findForkDescriptionTextarea().attributes('value')).toBe(
DEFAULT_PROPS.projectDescription,
);
});
it('sets project URL prepend text with gon.gitlab_url', () => {
mockGetRequest();
createComponent();
expect(wrapper.find(GlFormInputGroup).text()).toContain(`${GON_GITLAB_URL}/`);
});
it('will have required attribute for required fields', () => {
mockGetRequest();
createComponent();
expect(findForkNameInput().attributes('required')).not.toBeUndefined();
expect(findForkUrlInput().attributes('required')).not.toBeUndefined();
expect(findForkSlugInput().attributes('required')).not.toBeUndefined();
expect(findVisibilityRadioGroup().attributes('required')).not.toBeUndefined();
expect(findForkDescriptionTextarea().attributes('required')).toBeUndefined();
});
describe('forks namespaces', () => {
beforeEach(() => {
mockGetRequest({ namespaces: MOCK_NAMESPACES_RESPONSE });
createComponent();
});
it('make GET request from endpoint', async () => {
await axios.waitForAll();
expect(axiosMock.history.get[0].url).toBe(DEFAULT_PROPS.endpoint);
});
it('generate default option', async () => {
await axios.waitForAll();
const optionsArray = findForkUrlInput().findAll('option');
expect(optionsArray.at(0).text()).toBe('Select a namespace');
});
it('populate project url namespace options', async () => {
await axios.waitForAll();
const optionsArray = findForkUrlInput().findAll('option');
expect(optionsArray).toHaveLength(MOCK_NAMESPACES_RESPONSE.length + 1);
expect(optionsArray.at(1).text()).toBe(MOCK_NAMESPACES_RESPONSE[0].name);
expect(optionsArray.at(2).text()).toBe(MOCK_NAMESPACES_RESPONSE[1].name);
});
});
describe('visibility level', () => {
it.each`
project | namespace | privateIsDisabled | internalIsDisabled | publicIsDisabled
${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'}
${'private'} | ${'internal'} | ${undefined} | ${'true'} | ${'true'}
${'private'} | ${'public'} | ${undefined} | ${'true'} | ${'true'}
${'internal'} | ${'private'} | ${undefined} | ${'true'} | ${'true'}
${'internal'} | ${'internal'} | ${undefined} | ${undefined} | ${'true'}
${'internal'} | ${'public'} | ${undefined} | ${undefined} | ${'true'}
${'public'} | ${'private'} | ${undefined} | ${'true'} | ${'true'}
${'public'} | ${'internal'} | ${undefined} | ${undefined} | ${'true'}
${'public'} | ${'public'} | ${undefined} | ${undefined} | ${undefined}
`(
'sets appropriate radio button disabled state',
async ({ project, namespace, privateIsDisabled, internalIsDisabled, publicIsDisabled }) => {
mockGetRequest();
createComponent(
{
projectVisibility: project,
},
{
selectedNamespace: {
visibility: namespace,
},
},
);
expect(findPrivateRadio().attributes('disabled')).toBe(privateIsDisabled);
expect(findInternalRadio().attributes('disabled')).toBe(internalIsDisabled);
expect(findPublicRadio().attributes('disabled')).toBe(publicIsDisabled);
},
);
});
describe('onSubmit', () => {
beforeEach(() => {
jest.spyOn(urlUtility, 'redirectTo').mockImplementation();
});
it('redirect to POST web_url response', async () => {
const webUrl = `new/fork-project`;
jest.spyOn(axios, 'post').mockResolvedValue({ data: { web_url: webUrl } });
mockGetRequest();
createComponent();
await wrapper.vm.onSubmit();
expect(urlUtility.redirectTo).toHaveBeenCalledWith(webUrl);
});
it('display flash when POST is unsuccessful', async () => {
const dummyError = 'Fork project failed';
jest.spyOn(axios, 'post').mockRejectedValue(dummyError);
mockGetRequest();
createComponent();
await wrapper.vm.onSubmit();
expect(urlUtility.redirectTo).not.toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
message: dummyError,
});
});
});
});
import { GlDropdown } from '@gitlab/ui'; import { GlDropdown } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils'; import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import Breadcrumbs from '~/repository/components/breadcrumbs.vue'; import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
describe('Repository breadcrumbs component', () => { let vm;
let wrapper;
const factory = (currentPath, extraProps = {}) => { function factory(currentPath, extraProps = {}) {
const $apollo = { vm = shallowMount(Breadcrumbs, {
queries: {
userPermissions: {
loading: true,
},
},
};
wrapper = shallowMount(Breadcrumbs, {
propsData: { propsData: {
currentPath, currentPath,
...extraProps, ...extraProps,
...@@ -23,14 +13,12 @@ describe('Repository breadcrumbs component', () => { ...@@ -23,14 +13,12 @@ describe('Repository breadcrumbs component', () => {
stubs: { stubs: {
RouterLink: RouterLinkStub, RouterLink: RouterLinkStub,
}, },
mocks: { $apollo },
}); });
}; }
const findUploadBlobModal = () => wrapper.find(UploadBlobModal);
describe('Repository breadcrumbs component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); vm.destroy();
}); });
it.each` it.each`
...@@ -42,13 +30,13 @@ describe('Repository breadcrumbs component', () => { ...@@ -42,13 +30,13 @@ describe('Repository breadcrumbs component', () => {
`('renders $linkCount links for path $path', ({ path, linkCount }) => { `('renders $linkCount links for path $path', ({ path, linkCount }) => {
factory(path); factory(path);
expect(wrapper.findAll(RouterLinkStub).length).toEqual(linkCount); expect(vm.findAll(RouterLinkStub).length).toEqual(linkCount);
}); });
it('escapes hash in directory path', () => { it('escapes hash in directory path', () => {
factory('app/assets/javascripts#'); factory('app/assets/javascripts#');
expect(wrapper.findAll(RouterLinkStub).at(3).props('to')).toEqual( expect(vm.findAll(RouterLinkStub).at(3).props('to')).toEqual(
'/-/tree/app/assets/javascripts%23', '/-/tree/app/assets/javascripts%23',
); );
}); });
...@@ -56,44 +44,26 @@ describe('Repository breadcrumbs component', () => { ...@@ -56,44 +44,26 @@ describe('Repository breadcrumbs component', () => {
it('renders last link as active', () => { it('renders last link as active', () => {
factory('app/assets'); factory('app/assets');
expect(wrapper.findAll(RouterLinkStub).at(2).attributes('aria-current')).toEqual('page'); expect(vm.findAll(RouterLinkStub).at(2).attributes('aria-current')).toEqual('page');
}); });
it('does not render add to tree dropdown when permissions are false', async () => { it('does not render add to tree dropdown when permissions are false', () => {
factory('/', { canCollaborate: false }); factory('/', { canCollaborate: false });
wrapper.setData({ userPermissions: { forkProject: false, createMergeRequestIn: false } }); vm.setData({ userPermissions: { forkProject: false, createMergeRequestIn: false } });
await wrapper.vm.$nextTick();
expect(wrapper.find(GlDropdown).exists()).toBe(false); return vm.vm.$nextTick(() => {
expect(vm.find(GlDropdown).exists()).toBe(false);
}); });
it('renders add to tree dropdown when permissions are true', async () => {
factory('/', { canCollaborate: true });
wrapper.setData({ userPermissions: { forkProject: true, createMergeRequestIn: true } });
await wrapper.vm.$nextTick();
expect(wrapper.find(GlDropdown).exists()).toBe(true);
});
describe('renders the upload blob modal', () => {
beforeEach(() => {
factory('/', { canEditTree: true });
});
it('does not render the modal while loading', () => {
expect(findUploadBlobModal().exists()).toBe(false);
}); });
it('renders the modal once loaded', async () => { it('renders add to tree dropdown when permissions are true', () => {
wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } }); factory('/', { canCollaborate: true });
await wrapper.vm.$nextTick(); vm.setData({ userPermissions: { forkProject: true, createMergeRequestIn: true } });
expect(findUploadBlobModal().exists()).toBe(true); return vm.vm.$nextTick(() => {
expect(vm.find(GlDropdown).exists()).toBe(true);
}); });
}); });
}); });
import { GlModal, GlFormInput, GlFormTextarea, GlToggle, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { visitUrl } 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';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
joinPaths: () => '/new_upload',
}));
const initialProps = {
modalId: 'upload-blob',
commitMessage: 'Upload New File',
targetBranch: 'master',
origionalBranch: 'master',
canPushCode: true,
path: 'new_upload',
};
describe('UploadBlobModal', () => {
let wrapper;
let mock;
const mockEvent = { preventDefault: jest.fn() };
const createComponent = (props) => {
wrapper = shallowMount(UploadBlobModal, {
propsData: {
...initialProps,
...props,
},
mocks: {
$route: {
params: {
path: '',
},
},
},
});
};
const findModal = () => wrapper.find(GlModal);
const findAlert = () => wrapper.find(GlAlert);
const findCommitMessage = () => wrapper.find(GlFormTextarea);
const findBranchName = () => wrapper.find(GlFormInput);
const findMrToggle = () => wrapper.find(GlToggle);
const findUploadDropzone = () => wrapper.find(UploadDropzone);
const actionButtonDisabledState = () => findModal().props('actionPrimary').attributes[0].disabled;
const cancelButtonDisabledState = () => findModal().props('actionCancel').attributes[0].disabled;
const actionButtonLoadingState = () => findModal().props('actionPrimary').attributes[0].loading;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe.each`
canPushCode | displayBranchName | displayForkedBranchMessage
${true} | ${true} | ${false}
${false} | ${false} | ${true}
`(
'canPushCode = $canPushCode',
({ canPushCode, displayBranchName, displayForkedBranchMessage }) => {
beforeEach(() => {
createComponent({ canPushCode });
});
it('displays the modal', () => {
expect(findModal().exists()).toBe(true);
});
it('includes the upload dropzone', () => {
expect(findUploadDropzone().exists()).toBe(true);
});
it('includes the commit message', () => {
expect(findCommitMessage().exists()).toBe(true);
});
it('displays the disabled upload button', () => {
expect(actionButtonDisabledState()).toBe(true);
});
it('displays the enabled cancel button', () => {
expect(cancelButtonDisabledState()).toBe(false);
});
it('does not display the MR toggle', () => {
expect(findMrToggle().exists()).toBe(false);
});
it(`${
displayForkedBranchMessage ? 'displays' : 'does not display'
} the forked branch message`, () => {
expect(findAlert().exists()).toBe(displayForkedBranchMessage);
});
it(`${displayBranchName ? 'displays' : 'does not display'} the branch name`, () => {
expect(findBranchName().exists()).toBe(displayBranchName);
});
if (canPushCode) {
describe('when changing the branch name', () => {
it('displays the MR toggle', async () => {
wrapper.setData({ target: 'Not master' });
await wrapper.vm.$nextTick();
expect(findMrToggle().exists()).toBe(true);
});
});
}
describe('completed form', () => {
beforeEach(() => {
wrapper.setData({
file: { type: 'jpg' },
filePreviewURL: 'http://file.com?format=jpg',
});
});
it('enables the upload button when the form is completed', () => {
expect(actionButtonDisabledState()).toBe(false);
});
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);
});
});
describe('successful response', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
mock.onPost(initialProps.path).reply(httpStatusCodes.OK, { filePath: 'blah' });
findModal().vm.$emit('primary', mockEvent);
await waitForPromises();
});
it('redirects to the uploaded file', () => {
expect(visitUrl).toHaveBeenCalled();
});
afterEach(() => {
mock.restore();
});
});
describe('error response', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
mock.onPost(initialProps.path).timeout();
findModal().vm.$emit('primary', mockEvent);
await waitForPromises();
});
it('creates a flash error', () => {
expect(createFlash).toHaveBeenCalledWith('Error uploading file. Please try again.');
});
afterEach(() => {
mock.restore();
});
});
});
},
);
});
...@@ -10,7 +10,7 @@ RSpec.shared_examples 'it uploads and commit a new text file' do ...@@ -10,7 +10,7 @@ RSpec.shared_examples 'it uploads and commit a new text file' do
wait_for_requests wait_for_requests
end end
attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true) drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
page.within('#modal-upload-blob') do page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message') fill_in(:commit_message, with: 'New commit message')
...@@ -42,7 +42,7 @@ RSpec.shared_examples 'it uploads and commit a new image file' do ...@@ -42,7 +42,7 @@ RSpec.shared_examples 'it uploads and commit a new image file' do
wait_for_requests wait_for_requests
end end
attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg'), make_visible: true) drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg'))
page.within('#modal-upload-blob') do page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message') fill_in(:commit_message, with: 'New commit message')
...@@ -70,11 +70,9 @@ RSpec.shared_examples 'it uploads and commit a new file to a forked project' do ...@@ -70,11 +70,9 @@ RSpec.shared_examples 'it uploads and commit a new file to a forked project' do
expect(page).to have_content(fork_message) expect(page).to have_content(fork_message)
wait_for_all_requests
find('.add-to-tree').click find('.add-to-tree').click
click_link('Upload file') click_link('Upload file')
attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true) drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
page.within('#modal-upload-blob') do page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message') fill_in(:commit_message, with: 'New commit message')
...@@ -96,30 +94,3 @@ RSpec.shared_examples 'it uploads and commit a new file to a forked project' do ...@@ -96,30 +94,3 @@ RSpec.shared_examples 'it uploads and commit a new file to a forked project' do
expect(page).to have_content('Sed ut perspiciatis unde omnis') expect(page).to have_content('Sed ut perspiciatis unde omnis')
end end
end end
RSpec.shared_examples 'it uploads a file to a sub-directory' do
it 'uploads a file to a sub-directory', :js do
click_link 'files'
page.within('.repo-breadcrumb') do
expect(page).to have_content('files')
end
find('.add-to-tree').click
click_link('Upload file')
attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true)
page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message')
end
click_button('Upload file')
expect(page).to have_content('New commit message')
page.within('.repo-breadcrumb') do
expect(page).to have_content('files')
expect(page).to have_content('doc_sample.txt')
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment