Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-03 12:10:53 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-03 12:10:53 +0300
commit74780f24f2005d24a0e0a8fa1b3ae5391ae2928f (patch)
tree888505246a85ff9e97042b43b18450730586596e
parentccbe90951fb75b3527eaaad404e6abb6ed09ca8c (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml10
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue304
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/index.js61
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue53
-rw-r--r--app/assets/javascripts/repository/components/upload_blob_modal.vue218
-rw-r--r--app/assets/javascripts/repository/index.js15
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss4
-rw-r--r--app/controllers/projects/forks_controller.rb4
-rw-r--r--app/graphql/types/todo_action_enum.rb16
-rw-r--r--app/helpers/tree_helper.rb2
-rw-r--r--app/views/dashboard/projects/_projects.html.haml2
-rw-r--r--app/views/projects/_files.html.haml1
-rw-r--r--app/views/projects/forks/new.html.haml24
-rw-r--r--app/views/projects/runners/_runner.html.haml2
-rw-r--r--changelogs/unreleased/209061-remove-dashboard-pipeline-status-ff.yml5
-rw-r--r--changelogs/unreleased/254280-replace-bootstrap-modal-trigger-in-app-assets-javascripts-blob-blo.yml5
-rw-r--r--changelogs/unreleased/gl-badge-runners.yml5
-rw-r--r--changelogs/unreleased/sfang-do-not-show-token-name.yml5
-rw-r--r--config/feature_flags/development/fork_project_form.yml (renamed from config/feature_flags/development/dashboard_pipeline_status.yml)12
-rw-r--r--doc/api/graphql/reference/index.md16
-rw-r--r--lib/api/entities/user_safe.rb3
-rw-r--r--locale/gitlab.pot45
-rw-r--r--spec/features/dashboard/projects_spec.rb8
-rw-r--r--spec/features/projects/files/user_uploads_files_spec.rb29
-rw-r--r--spec/features/projects/fork_spec.rb1
-rw-r--r--spec/features/projects/members/list_spec.rb2
-rw-r--r--spec/features/projects/show/user_uploads_files_spec.rb4
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js273
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js84
-rw-r--r--spec/frontend/repository/components/upload_blob_modal_spec.js193
-rw-r--r--spec/graphql/features/authorization_spec.rb37
-rw-r--r--spec/graphql/features/feature_flag_spec.rb3
-rw-r--r--spec/graphql/mutations/release_asset_links/create_spec.rb10
-rw-r--r--spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb3
-rw-r--r--spec/graphql/resolvers/error_tracking/sentry_errors_resolver_spec.rb6
-rw-r--r--spec/graphql/resolvers/group_labels_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb8
-rw-r--r--spec/graphql/resolvers/labels_resolver_spec.rb36
-rw-r--r--spec/graphql/resolvers/merge_requests_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/release_milestones_resolver_spec.rb2
-rw-r--r--spec/lib/api/entities/user_spec.rb18
-rw-r--r--spec/support/graphql/resolver_factories.rb40
-rw-r--r--spec/support/helpers/features/members_table_helpers.rb4
-rw-r--r--spec/support/helpers/graphql_helpers.rb197
-rw-r--r--spec/support/shared_examples/features/project_upload_files_shared_examples.rb35
45 files changed, 1066 insertions, 743 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index ac4763103b2..ef66c8aeb2e 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -405,6 +405,7 @@
- <<: *if-security-merge-request
changes: *code-backstage-patterns
- <<: *if-merge-request-title-as-if-foss
+ - <<: *if-merge-request-title-run-all-rspec
- <<: *if-merge-request
changes: *ci-patterns
@@ -483,6 +484,7 @@
- <<: *if-security-merge-request
changes: *code-qa-patterns
- <<: *if-merge-request-title-as-if-foss
+ - <<: *if-merge-request-title-run-all-rspec
- <<: *if-merge-request
changes: *ci-patterns
@@ -700,6 +702,7 @@
changes: *db-patterns
- <<: *if-merge-request-title-as-if-foss
changes: *db-patterns
+ - <<: *if-merge-request-title-run-all-rspec
- <<: *if-merge-request
changes: *ci-patterns
@@ -716,6 +719,7 @@
changes: *db-patterns
- <<: *if-merge-request-title-as-if-foss
changes: *db-patterns
+ - <<: *if-merge-request-title-run-all-rspec
.rails:rules:as-if-foss-unit:
rules:
@@ -725,6 +729,7 @@
changes: *backend-patterns
- <<: *if-merge-request-title-as-if-foss
changes: *backend-patterns
+ - <<: *if-merge-request-title-run-all-rspec
- <<: *if-merge-request
changes: *ci-patterns
@@ -741,6 +746,7 @@
changes: *backend-patterns
- <<: *if-merge-request-title-as-if-foss
changes: *backend-patterns
+ - <<: *if-merge-request-title-run-all-rspec
.rails:rules:as-if-foss-integration:
rules:
@@ -750,6 +756,7 @@
changes: *backend-patterns
- <<: *if-merge-request-title-as-if-foss
changes: *backend-patterns
+ - <<: *if-merge-request-title-run-all-rspec
- <<: *if-merge-request
changes: *ci-patterns
@@ -766,6 +773,7 @@
changes: *backend-patterns
- <<: *if-merge-request-title-as-if-foss
changes: *backend-patterns
+ - <<: *if-merge-request-title-run-all-rspec
.rails:rules:as-if-foss-system:
rules:
@@ -775,6 +783,7 @@
changes: *code-backstage-patterns
- <<: *if-merge-request-title-as-if-foss
changes: *code-backstage-patterns
+ - <<: *if-merge-request-title-run-all-rspec
- <<: *if-merge-request
changes: *ci-patterns
@@ -791,6 +800,7 @@
changes: *code-backstage-patterns
- <<: *if-merge-request-title-as-if-foss
changes: *code-backstage-patterns
+ - <<: *if-merge-request-title-run-all-rspec
.rails:rules:ee-and-foss-db-library-code:
rules:
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
new file mode 100644
index 00000000000..a59a8afa4de
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -0,0 +1,304 @@
+<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>
diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js
index 26353aefe82..420639eefb7 100644
--- a/app/assets/javascripts/pages/projects/forks/new/index.js
+++ b/app/assets/javascripts/pages/projects/forks/new/index.js
@@ -1,16 +1,53 @@
import Vue from 'vue';
+import ForkForm from './components/fork_form.vue';
import ForkGroupsList from './components/fork_groups_list.vue';
const mountElement = document.getElementById('fork-groups-mount-element');
-const { endpoint } = mountElement.dataset;
-// eslint-disable-next-line no-new
-new Vue({
- el: mountElement,
- render(h) {
- return h(ForkGroupsList, {
- props: {
- endpoint,
- },
- });
- },
-});
+
+if (gon.features.forkProjectForm) {
+ 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,
+ render(h) {
+ return h(ForkGroupsList, {
+ props: {
+ endpoint,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index 70167bde188..28d7dec85f4 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -5,7 +5,6 @@ import {
GlDropdownSectionHeader,
GlDropdownItem,
GlIcon,
- GlModalDirective,
} from '@gitlab/ui';
import permissionsQuery from 'shared_queries/repository/permissions.query.graphql';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
@@ -13,15 +12,12 @@ import { __ } from '../../locale';
import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
import projectShortPathQuery from '../queries/project_short_path.query.graphql';
-import UploadBlobModal from './upload_blob_modal.vue';
const ROW_TYPES = {
header: 'header',
divider: 'divider',
};
-const UPLOAD_BLOB_MODAL_ID = 'modal-upload-blob';
-
export default {
components: {
GlDropdown,
@@ -29,7 +25,6 @@ export default {
GlDropdownSectionHeader,
GlDropdownItem,
GlIcon,
- UploadBlobModal,
},
apollo: {
projectShortPath: {
@@ -51,9 +46,6 @@ export default {
},
},
},
- directives: {
- GlModal: GlModalDirective,
- },
mixins: [getRefMixin],
props: {
currentPath: {
@@ -71,21 +63,6 @@ export default {
required: false,
default: false,
},
- canPushCode: {
- type: Boolean,
- required: false,
- default: false,
- },
- selectedBranch: {
- type: String,
- required: false,
- default: '',
- },
- origionalBranch: {
- type: String,
- required: false,
- default: '',
- },
newBranchPath: {
type: String,
required: false,
@@ -116,13 +93,7 @@ export default {
required: false,
default: null,
},
- uploadPath: {
- type: String,
- required: false,
- default: '',
- },
},
- uploadBlobModalId: UPLOAD_BLOB_MODAL_ID,
data() {
return {
projectShortPath: '',
@@ -155,10 +126,7 @@ export default {
);
},
canCreateMrFromFork() {
- return this.userPermissions?.forkProject && this.userPermissions?.createMergeRequestIn;
- },
- showUploadModal() {
- return this.canEditTree && !this.$apollo.queries.userPermissions.loading;
+ return this.userPermissions.forkProject && this.userPermissions.createMergeRequestIn;
},
dropdownItems() {
const items = [];
@@ -181,9 +149,10 @@ export default {
{
attrs: {
href: '#modal-upload-blob',
+ 'data-target': '#modal-upload-blob',
+ 'data-toggle': 'modal',
},
text: __('Upload file'),
- modalId: UPLOAD_BLOB_MODAL_ID,
},
{
attrs: {
@@ -284,26 +253,12 @@ export default {
<gl-icon name="chevron-down" :size="16" class="float-left" />
</template>
<template v-for="(item, i) in dropdownItems">
- <component
- :is="getComponent(item.type)"
- :key="i"
- v-bind="item.attrs"
- v-gl-modal="item.modalId || null"
- >
+ <component :is="getComponent(item.type)" :key="i" v-bind="item.attrs">
{{ item.text }}
</component>
</template>
</gl-dropdown>
</li>
</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>
</template>
diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue
deleted file mode 100644
index 4cdfc5e947a..00000000000
--- a/app/assets/javascripts/repository/components/upload_blob_modal.vue
+++ /dev/null
@@ -1,218 +0,0 @@
-<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>
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index fafc6e8eb89..747b85f5c1c 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -1,8 +1,8 @@
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 { parseBoolean } from '../lib/utils/common_utils';
+import { escapeFileUrl } from '../lib/utils/url_utility';
+import { __ } from '../locale';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
@@ -55,8 +55,6 @@ export default function setupVueRepositoryList() {
const {
canCollaborate,
canEditTree,
- canPushCode,
- selectedBranch,
newBranchPath,
newTagPath,
newBlobPath,
@@ -67,7 +65,8 @@ export default function setupVueRepositoryList() {
newDirPath,
} = breadcrumbEl.dataset;
- router.afterEach(({ params: { path } }) => {
+ router.afterEach(({ params: { path = '/' } }) => {
+ updateFormAction('.js-upload-blob-form', uploadPath, path);
updateFormAction('.js-create-dir-form', newDirPath, path);
});
@@ -82,16 +81,12 @@ export default function setupVueRepositoryList() {
currentPath: this.$route.params.path,
canCollaborate: parseBoolean(canCollaborate),
canEditTree: parseBoolean(canEditTree),
- canPushCode: parseBoolean(canPushCode),
- origionalBranch: ref,
- selectedBranch,
newBranchPath,
newTagPath,
newBlobPath,
forkNewBlobPath,
forkNewDirectoryPath,
forkUploadBlobPath,
- uploadPath,
},
});
},
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index 1cca42f94b2..e0543c5683b 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -37,6 +37,10 @@
}
}
+[data-page$='epic_boards:show'] .filter-form {
+ display: none;
+}
+
.boards-app {
@include media-breakpoint-up(sm) {
transition: width $sidebar-transition-duration;
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 5576d5766c7..33f046f414f 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -16,6 +16,10 @@ class Projects::ForksController < Projects::ApplicationController
feature_category :source_code_management
+ before_action do
+ push_frontend_feature_flag(:fork_project_form)
+ end
+
def index
@total_forks_count = project.forks.size
@public_forks_count = project.forks.public_only.size
diff --git a/app/graphql/types/todo_action_enum.rb b/app/graphql/types/todo_action_enum.rb
index 0e538838474..ef43b6eb464 100644
--- a/app/graphql/types/todo_action_enum.rb
+++ b/app/graphql/types/todo_action_enum.rb
@@ -2,12 +2,14 @@
module Types
class TodoActionEnum < BaseEnum
- value 'assigned', value: 1
- value 'mentioned', value: 2
- value 'build_failed', value: 3
- value 'marked', value: 4
- value 'approval_required', value: 5
- value 'unmergeable', value: 6
- value 'directly_addressed', value: 7
+ value 'assigned', value: 1, description: 'User was assigned.'
+ value 'mentioned', value: 2, description: 'User was mentioned.'
+ value 'build_failed', value: 3, description: 'Build triggered by the user failed.'
+ value 'marked', value: 4, description: 'User added a TODO.'
+ value 'approval_required', value: 5, description: 'User was set as an approver.'
+ value 'unmergeable', value: 6, description: 'Merge request authored by the user could not be merged.'
+ value 'directly_addressed', value: 7, description: 'User was directly addressed.'
+ value 'merge_train_removed', value: 8, description: 'Merge request authored by the user was removed from the merge train.'
+ value 'review_requested', value: 9, description: 'Review was requested from the user.'
end
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index b795851ba30..b050f533d77 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -131,8 +131,6 @@ module TreeHelper
def breadcrumb_data_attributes
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,
new_blob_path: project_new_blob_path(@project, @ref),
upload_path: project_create_blob_path(@project, @ref),
diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml
index 5122164dbcb..06ef89e0b57 100644
--- a/app/views/dashboard/projects/_projects.html.haml
+++ b/app/views/dashboard/projects/_projects.html.haml
@@ -1 +1 @@
-= render 'shared/projects/list', projects: @projects, pipeline_status: Feature.enabled?(:dashboard_pipeline_status, default_enabled: true), user: current_user
+= render 'shared/projects/list', projects: @projects, pipeline_status: true, user: current_user
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 0369ee50c40..30d885964b5 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -21,4 +21,5 @@
#js-tree-list{ data: vue_file_list_data(project, ref) }
- 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'
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index ccef28a2cf3..562aa0fd353 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -9,10 +9,20 @@
%br
= _('Forking a repository allows you to make changes without affecting the original project.')
.col-lg-9
- - if @own_namespace.present?
- .fork-thumbnail-container.js-fork-content
- %h5.gl-mt-0.gl-mb-0.gl-ml-3.gl-mr-3
- = _("Select a namespace to fork the project")
- = render 'fork_button', namespace: @own_namespace
- #fork-groups-mount-element{ data: { endpoint: new_project_fork_path(@project, format: :json) } }
-
+ - 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?
+ .fork-thumbnail-container.js-fork-content
+ %h5.gl-mt-0.gl-mb-0.gl-ml-3.gl-mr-3
+ = _("Select a namespace to fork the project")
+ = render 'fork_button', namespace: @own_namespace
+ #fork-groups-mount-element{ data: { endpoint: new_project_fork_path(@project, format: :json) } }
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 41159df1435..7f5acbbe890 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -40,5 +40,5 @@
- if runner.tags.present?
%p
- runner.tags.map(&:name).sort.each do |tag|
- %span.badge.badge-primary
+ %span.badge.gl-badge.sm.badge-pill.badge-primary
= tag
diff --git a/changelogs/unreleased/209061-remove-dashboard-pipeline-status-ff.yml b/changelogs/unreleased/209061-remove-dashboard-pipeline-status-ff.yml
new file mode 100644
index 00000000000..5abe0575a0f
--- /dev/null
+++ b/changelogs/unreleased/209061-remove-dashboard-pipeline-status-ff.yml
@@ -0,0 +1,5 @@
+---
+title: Remove dashboard_pipeline_status feature flag
+merge_request: 55472
+author:
+type: other
diff --git a/changelogs/unreleased/254280-replace-bootstrap-modal-trigger-in-app-assets-javascripts-blob-blo.yml b/changelogs/unreleased/254280-replace-bootstrap-modal-trigger-in-app-assets-javascripts-blob-blo.yml
deleted file mode 100644
index efb9ccf59a4..00000000000
--- a/changelogs/unreleased/254280-replace-bootstrap-modal-trigger-in-app-assets-javascripts-blob-blo.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Migrate bootstrap modal to GlModal for repo single file uploads
-merge_request: 53623
-author:
-type: changed
diff --git a/changelogs/unreleased/gl-badge-runners.yml b/changelogs/unreleased/gl-badge-runners.yml
new file mode 100644
index 00000000000..9b572401264
--- /dev/null
+++ b/changelogs/unreleased/gl-badge-runners.yml
@@ -0,0 +1,5 @@
+---
+title: Apply new GitLab UI for badge in runners list
+merge_request: 54766
+author: Yogi (@yo)
+type: changed
diff --git a/changelogs/unreleased/sfang-do-not-show-token-name.yml b/changelogs/unreleased/sfang-do-not-show-token-name.yml
new file mode 100644
index 00000000000..d4c337a8704
--- /dev/null
+++ b/changelogs/unreleased/sfang-do-not-show-token-name.yml
@@ -0,0 +1,5 @@
+---
+title: Do not expose user name if user is project bot
+merge_request: 54022
+author:
+type: changed
diff --git a/config/feature_flags/development/dashboard_pipeline_status.yml b/config/feature_flags/development/fork_project_form.yml
index f24ba5983a8..93bccc4f41b 100644
--- a/config/feature_flags/development/dashboard_pipeline_status.yml
+++ b/config/feature_flags/development/fork_project_form.yml
@@ -1,8 +1,8 @@
---
-name: dashboard_pipeline_status
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22029
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/209061
-milestone: '12.7'
+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::continuous integration
-default_enabled: true
+group: group::source code
+default_enabled: false
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index d995b57f275..8fbd425d118 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -5858,13 +5858,15 @@ State of a test report.
| Value | Description |
| ----- | ----------- |
-| `approval_required` | |
-| `assigned` | |
-| `build_failed` | |
-| `directly_addressed` | |
-| `marked` | |
-| `mentioned` | |
-| `unmergeable` | |
+| `approval_required` | User was set as an approver. |
+| `assigned` | User was assigned. |
+| `build_failed` | Build triggered by the user failed. |
+| `directly_addressed` | User was directly addressed. |
+| `marked` | User added a TODO. |
+| `mentioned` | User was mentioned. |
+| `merge_train_removed` | Merge request authored by the user was removed from the merge train. |
+| `review_requested` | Review was requested from the user. |
+| `unmergeable` | Merge request authored by the user could not be merged. |
### TodoStateEnum
diff --git a/lib/api/entities/user_safe.rb b/lib/api/entities/user_safe.rb
index feb01767fd6..2b7c14cba6e 100644
--- a/lib/api/entities/user_safe.rb
+++ b/lib/api/entities/user_safe.rb
@@ -3,7 +3,8 @@
module API
module Entities
class UserSafe < Grape::Entity
- expose :id, :name, :username
+ expose :id, :username
+ expose :name, unless: ->(user) { user.project_bot? && !options[:current_user].admin?}
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index bc98ce39dd7..4c4f25cf7a4 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -12036,9 +12036,6 @@ msgstr ""
msgid "Error uploading file"
msgstr ""
-msgid "Error uploading file. Please try again."
-msgstr ""
-
msgid "Error uploading file: %{stripped}"
msgstr ""
@@ -13262,6 +13259,42 @@ msgstr ""
msgid "Fork project?"
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"
msgstr ""
@@ -24967,9 +25000,6 @@ msgstr ""
msgid "Remove due date"
msgstr ""
-msgid "Remove file"
-msgstr ""
-
msgid "Remove fork relationship"
msgstr ""
@@ -28333,9 +28363,6 @@ msgstr ""
msgid "Start a new merge request"
msgstr ""
-msgid "Start a new merge request with these changes"
-msgstr ""
-
msgid "Start a review"
msgstr ""
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 8705c22c41a..d7330b5267b 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -198,14 +198,6 @@ RSpec.describe 'Dashboard Projects' do
it_behaves_like 'hidden pipeline status'
end
- context 'when dashboard_pipeline_status is disabled' do
- before do
- stub_feature_flags(dashboard_pipeline_status: false)
- end
-
- it_behaves_like 'hidden pipeline status'
- end
-
context "when last_pipeline is missing" do
before do
project.last_pipeline.delete
diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb
index e8f51837ec9..944d08df3f3 100644
--- a/spec/features/projects/files/user_uploads_files_spec.rb
+++ b/spec/features/projects/files/user_uploads_files_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > User uploads files' do
+ include DropzoneHelper
+
let(:user) { create(:user) }
let(:project) { create(:project, :repository, name: 'Shop', creator: user) }
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
@@ -15,15 +17,36 @@ RSpec.describe 'Projects > Files > User uploads files' do
context 'when a user has write access' do
before do
visit(project_tree_path(project))
-
- wait_for_requests
end
include_examples 'it uploads and commit a new text 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
context 'when a user does not have write access' do
diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb
index 8d0500f5e13..59bed5501f2 100644
--- a/spec/features/projects/fork_spec.rb
+++ b/spec/features/projects/fork_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe 'Project fork' do
let(:project) { create(:project, :public, :repository) }
before do
+ stub_feature_flags(fork_project_form: false)
sign_in(user)
end
diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb
index f1fc3927b03..3cc3c763e29 100644
--- a/spec/features/projects/members/list_spec.rb
+++ b/spec/features/projects/members/list_spec.rb
@@ -127,7 +127,7 @@ RSpec.describe 'Project members list' do
it 'does not show form used to change roles and "Expiration date" or the remove user button' do
visit_members_page
- page.within find_member_row(project_bot) do
+ page.within find_username_row(project_bot) do
expect(page).not_to have_button('Maintainer')
expect(page).to have_field('Expiration date', disabled: true)
expect(page).not_to have_button('Remove member')
diff --git a/spec/features/projects/show/user_uploads_files_spec.rb b/spec/features/projects/show/user_uploads_files_spec.rb
index 5749ffd0650..b7c5d324d93 100644
--- a/spec/features/projects/show/user_uploads_files_spec.rb
+++ b/spec/features/projects/show/user_uploads_files_spec.rb
@@ -17,15 +17,11 @@ RSpec.describe 'Projects > Show > User uploads files' do
context 'when a user has write access' do
before do
visit(project_path(project))
-
- wait_for_requests
end
include_examples 'it uploads and commit a new text file'
include_examples 'it uploads and commit a new image file'
-
- include_examples 'it uploads a file to a sub-directory'
end
context 'when a user does not have write access' do
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
new file mode 100644
index 00000000000..5aafb1e8d2e
--- /dev/null
+++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
@@ -0,0 +1,273 @@
+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,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index 93bfd3d9d32..2ac2069a177 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -1,36 +1,24 @@
import { GlDropdown } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
-import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
-describe('Repository breadcrumbs component', () => {
- let wrapper;
-
- const factory = (currentPath, extraProps = {}) => {
- const $apollo = {
- queries: {
- userPermissions: {
- loading: true,
- },
- },
- };
-
- wrapper = shallowMount(Breadcrumbs, {
- propsData: {
- currentPath,
- ...extraProps,
- },
- stubs: {
- RouterLink: RouterLinkStub,
- },
- mocks: { $apollo },
- });
- };
-
- const findUploadBlobModal = () => wrapper.find(UploadBlobModal);
+let vm;
+
+function factory(currentPath, extraProps = {}) {
+ vm = shallowMount(Breadcrumbs, {
+ propsData: {
+ currentPath,
+ ...extraProps,
+ },
+ stubs: {
+ RouterLink: RouterLinkStub,
+ },
+ });
+}
+describe('Repository breadcrumbs component', () => {
afterEach(() => {
- wrapper.destroy();
+ vm.destroy();
});
it.each`
@@ -42,13 +30,13 @@ describe('Repository breadcrumbs component', () => {
`('renders $linkCount links for path $path', ({ path, linkCount }) => {
factory(path);
- expect(wrapper.findAll(RouterLinkStub).length).toEqual(linkCount);
+ expect(vm.findAll(RouterLinkStub).length).toEqual(linkCount);
});
it('escapes hash in directory path', () => {
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',
);
});
@@ -56,44 +44,26 @@ describe('Repository breadcrumbs component', () => {
it('renders last link as active', () => {
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 });
- 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 () => {
+ it('renders add to tree dropdown when permissions are true', () => {
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 () => {
- wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
-
- 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);
});
});
});
diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js
deleted file mode 100644
index 6e3cbad558d..00000000000
--- a/spec/frontend/repository/components/upload_blob_modal_spec.js
+++ /dev/null
@@ -1,193 +0,0 @@
-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();
- });
- });
- });
- },
- );
-});
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb
index ec67ed16fe9..33b11e1ca09 100644
--- a/spec/graphql/features/authorization_spec.rb
+++ b/spec/graphql/features/authorization_spec.rb
@@ -2,17 +2,22 @@
require 'spec_helper'
-RSpec.describe 'Gitlab::Graphql::Authorization' do
+RSpec.describe 'Gitlab::Graphql::Authorize' do
include GraphqlHelpers
+ include Graphql::ResolverFactories
let_it_be(:user) { create(:user) }
let(:permission_single) { :foo }
let(:permission_collection) { [:foo, :bar] }
let(:test_object) { double(name: 'My name') }
let(:query_string) { '{ item { name } }' }
- let(:result) { execute_query(query_type)['data'] }
+ let(:result) do
+ schema = empty_schema
+ schema.use(Gitlab::Graphql::Authorize)
+ execute_query(query_type, schema: schema)
+ end
- subject { result['item'] }
+ subject { result.dig('data', 'item') }
shared_examples 'authorization with a single permission' do
it 'returns the protected field when user has permission' do
@@ -55,7 +60,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
describe 'with a single permission' do
let(:query_type) do
query_factory do |query|
- query.field :item, type, null: true, resolver: simple_resolver(test_object), authorize: permission_single
+ query.field :item, type, null: true, resolver: new_resolver(test_object), authorize: permission_single
end
end
@@ -66,7 +71,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
let(:query_type) do
permissions = permission_collection
query_factory do |qt|
- qt.field :item, type, null: true, resolver: simple_resolver(test_object) do
+ qt.field :item, type, null: true, resolver: new_resolver(test_object) do
authorize permissions
end
end
@@ -79,7 +84,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
describe 'Field authorizations when field is a built in type' do
let(:query_type) do
query_factory do |query|
- query.field :item, type, null: true, resolver: simple_resolver(test_object)
+ query.field :item, type, null: true, resolver: new_resolver(test_object)
end
end
@@ -132,7 +137,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
describe 'Type authorizations' do
let(:query_type) do
query_factory do |query|
- query.field :item, type, null: true, resolver: simple_resolver(test_object)
+ query.field :item, type, null: true, resolver: new_resolver(test_object)
end
end
@@ -169,7 +174,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
let(:query_type) do
query_factory do |query|
- query.field :item, type, null: true, resolver: simple_resolver(test_object), authorize: permission_2
+ query.field :item, type, null: true, resolver: new_resolver(test_object), authorize: permission_2
end
end
@@ -188,11 +193,11 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
let(:query_type) do
query_factory do |query|
- query.field :item, type.connection_type, null: true, resolver: simple_resolver([test_object, second_test_object])
+ query.field :item, type.connection_type, null: true, resolver: new_resolver([test_object, second_test_object])
end
end
- subject { result.dig('item', 'edges') }
+ subject { result.dig('data', 'item', 'edges') }
it 'returns only the elements visible to the user' do
permit(permission_single)
@@ -208,7 +213,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
describe 'limiting connections with multiple objects' do
let(:query_type) do
query_factory do |query|
- query.field :item, type.connection_type, null: true, resolver: simple_resolver([test_object, second_test_object])
+ query.field :item, type.connection_type, null: true, resolver: new_resolver([test_object, second_test_object])
end
end
@@ -232,11 +237,11 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
let(:query_type) do
query_factory do |query|
- query.field :item, [type], null: true, resolver: simple_resolver([test_object])
+ query.field :item, [type], null: true, resolver: new_resolver([test_object])
end
end
- subject { result['item'].first }
+ subject { result.dig('data', 'item', 0) }
include_examples 'authorization with a single permission'
end
@@ -260,13 +265,13 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
type_factory do |type|
type.graphql_name 'FakeProjectType'
type.field :test_issues, issue_type.connection_type, null: false,
- resolver: simple_resolver(Issue.where(project: [visible_project, other_project]).order(id: :asc))
+ resolver: new_resolver(Issue.where(project: [visible_project, other_project]).order(id: :asc))
end
end
let(:query_type) do
query_factory do |query|
- query.field :test_project, project_type, null: false, resolver: simple_resolver(visible_project)
+ query.field :test_project, project_type, null: false, resolver: new_resolver(visible_project)
end
end
@@ -281,7 +286,7 @@ RSpec.describe 'Gitlab::Graphql::Authorization' do
end
it 'renders the issues the user has access to' do
- issue_edges = result['testProject']['testIssues']['edges']
+ issue_edges = result.dig('data', 'testProject', 'testIssues', 'edges')
issue_ids = issue_edges.map { |issue_edge| issue_edge['node']&.fetch('id') }
expect(issue_edges.size).to eq(visible_issues.size)
diff --git a/spec/graphql/features/feature_flag_spec.rb b/spec/graphql/features/feature_flag_spec.rb
index 77810f78257..30238cf9cb3 100644
--- a/spec/graphql/features/feature_flag_spec.rb
+++ b/spec/graphql/features/feature_flag_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Graphql Field feature flags' do
include GraphqlHelpers
+ include Graphql::ResolverFactories
let_it_be(:user) { create(:user) }
@@ -23,7 +24,7 @@ RSpec.describe 'Graphql Field feature flags' do
let(:query_type) do
query_factory do |query|
- query.field :item, type, null: true, feature_flag: feature_flag, resolver: simple_resolver(test_object)
+ query.field :item, type, null: true, feature_flag: feature_flag, resolver: new_resolver(test_object)
end
end
diff --git a/spec/graphql/mutations/release_asset_links/create_spec.rb b/spec/graphql/mutations/release_asset_links/create_spec.rb
index e7bac6b3287..089bc3d3276 100644
--- a/spec/graphql/mutations/release_asset_links/create_spec.rb
+++ b/spec/graphql/mutations/release_asset_links/create_spec.rb
@@ -21,9 +21,9 @@ RSpec.describe Mutations::ReleaseAssetLinks::Create do
let(:args) do
{
project_path: project_path,
- tag: tag,
+ tag_name: tag,
name: name,
- filepath: filepath,
+ direct_asset_path: filepath,
url: url
}
end
@@ -44,9 +44,9 @@ RSpec.describe Mutations::ReleaseAssetLinks::Create do
expect(release.links.length).to be(1)
- expect(last_release_link.name).to eq(args[:name])
- expect(last_release_link.url).to eq(args[:url])
- expect(last_release_link.filepath).to eq(args[:filepath])
+ expect(last_release_link.name).to eq(name)
+ expect(last_release_link.url).to eq(url)
+ expect(last_release_link.filepath).to eq(filepath)
end
end
diff --git a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb
index 5370f7a7433..e9e7fff6e6e 100644
--- a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb
+++ b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb
@@ -9,7 +9,6 @@ RSpec.describe ::CachingArrayResolver do
let_it_be(:admins) { create_list(:user, 4, admin: true) }
let(:query_context) { { current_user: admins.first } }
let(:max_page_size) { 10 }
- let(:field) { double('Field', max_page_size: max_page_size) }
let(:schema) do
Class.new(GitlabSchema) do
default_max_page_size 3
@@ -210,6 +209,6 @@ RSpec.describe ::CachingArrayResolver do
args = { is_admin: admin }
opts = resolver.field_options
allow(resolver).to receive(:field_options).and_return(opts.merge(max_page_size: max_page_size))
- resolve(resolver, args: args, ctx: query_context, schema: schema, field: field)
+ resolve(resolver, args: args, ctx: query_context, schema: schema)
end
end
diff --git a/spec/graphql/resolvers/error_tracking/sentry_errors_resolver_spec.rb b/spec/graphql/resolvers/error_tracking/sentry_errors_resolver_spec.rb
index 170a602fb0d..68badb8e333 100644
--- a/spec/graphql/resolvers/error_tracking/sentry_errors_resolver_spec.rb
+++ b/spec/graphql/resolvers/error_tracking/sentry_errors_resolver_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Resolvers::ErrorTracking::SentryErrorsResolver do
end
describe '#resolve' do
- context 'insufficient user permission' do
+ context 'with insufficient user permission' do
let(:user) { create(:user) }
it 'returns nil' do
@@ -29,7 +29,7 @@ RSpec.describe Resolvers::ErrorTracking::SentryErrorsResolver do
end
end
- context 'user with permission' do
+ context 'with sufficient permission' do
before do
project.add_developer(current_user)
@@ -93,7 +93,7 @@ RSpec.describe Resolvers::ErrorTracking::SentryErrorsResolver do
end
it 'returns an externally paginated array' do
- expect(resolve_errors).to be_a Gitlab::Graphql::ExternallyPaginatedArray
+ expect(resolve_errors).to be_a Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection
end
end
end
diff --git a/spec/graphql/resolvers/group_labels_resolver_spec.rb b/spec/graphql/resolvers/group_labels_resolver_spec.rb
index ed94f12502a..3f4ad8760c0 100644
--- a/spec/graphql/resolvers/group_labels_resolver_spec.rb
+++ b/spec/graphql/resolvers/group_labels_resolver_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Resolvers::GroupLabelsResolver do
context 'without parent' do
it 'returns no labels' do
- expect(resolve_labels(nil)).to eq(Label.none)
+ expect(resolve_labels(nil)).to be_empty
end
end
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index 8980f4aa19d..6e802bf7d25 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -264,7 +264,7 @@ RSpec.describe Resolvers::IssuesResolver do
end
it 'finds a specific issue with iid', :request_store do
- result = batch_sync(max_queries: 4) { resolve_issues(iid: issue1.iid) }
+ result = batch_sync(max_queries: 4) { resolve_issues(iid: issue1.iid).to_a }
expect(result).to contain_exactly(issue1)
end
@@ -281,7 +281,7 @@ RSpec.describe Resolvers::IssuesResolver do
it 'finds a specific issue with iids', :request_store do
result = batch_sync(max_queries: 4) do
- resolve_issues(iids: [issue1.iid])
+ resolve_issues(iids: [issue1.iid]).to_a
end
expect(result).to contain_exactly(issue1)
@@ -290,7 +290,7 @@ RSpec.describe Resolvers::IssuesResolver do
it 'finds multiple issues with iids' do
create(:issue, project: project, author: current_user)
- expect(batch_sync { resolve_issues(iids: [issue1.iid, issue2.iid]) })
+ expect(batch_sync { resolve_issues(iids: [issue1.iid, issue2.iid]).to_a })
.to contain_exactly(issue1, issue2)
end
@@ -302,7 +302,7 @@ RSpec.describe Resolvers::IssuesResolver do
create(:issue, project: another_project, iid: iid)
end
- expect(batch_sync { resolve_issues(iids: iids) }).to contain_exactly(issue1, issue2)
+ expect(batch_sync { resolve_issues(iids: iids).to_a }).to contain_exactly(issue1, issue2)
end
end
end
diff --git a/spec/graphql/resolvers/labels_resolver_spec.rb b/spec/graphql/resolvers/labels_resolver_spec.rb
index 3d027a6c8d5..be6229553d7 100644
--- a/spec/graphql/resolvers/labels_resolver_spec.rb
+++ b/spec/graphql/resolvers/labels_resolver_spec.rb
@@ -42,50 +42,36 @@ RSpec.describe Resolvers::LabelsResolver do
context 'without parent' do
it 'returns no labels' do
- expect(resolve_labels(nil)).to eq(Label.none)
+ expect(resolve_labels(nil)).to be_empty
end
end
- context 'at project level' do
+ context 'with a parent project' do
before_all do
group.add_developer(current_user)
end
- # because :include_ancestor_groups, :include_descendant_groups, :only_group_labels default to false
- # the `nil` value would be equivalent to passing in `false` so just check for `nil` option
- where(:include_ancestor_groups, :include_descendant_groups, :only_group_labels, :search_term, :test) do
- nil | nil | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2) }
- nil | nil | true | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2) }
- nil | true | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
- nil | true | true | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
- true | nil | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2) }
- true | nil | true | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2) }
- true | true | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
- true | true | true | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
-
- nil | nil | nil | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2) }
- nil | nil | true | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2) }
- nil | true | nil | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2, sub_subgroup_label2) }
- nil | true | true | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2, sub_subgroup_label2) }
- true | nil | nil | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2) }
- true | nil | true | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2) }
- true | true | nil | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2, sub_subgroup_label2) }
- true | true | true | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2, sub_subgroup_label2) }
+ # the expected result is wrapped in a lambda to get around the phase restrictions of RSpec::Parameterized
+ where(:include_ancestor_groups, :search_term, :expected_labels) do
+ nil | nil | -> { [label1, label2, subgroup_label1, subgroup_label2] }
+ false | nil | -> { [label1, label2, subgroup_label1, subgroup_label2] }
+ true | nil | -> { [label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2] }
+ nil | 'new' | -> { [label2, subgroup_label2] }
+ false | 'new' | -> { [label2, subgroup_label2] }
+ true | 'new' | -> { [label2, group_label2, subgroup_label2] }
end
with_them do
let(:params) do
{
include_ancestor_groups: include_ancestor_groups,
- include_descendant_groups: include_descendant_groups,
- only_group_labels: only_group_labels,
search_term: search_term
}
end
subject { resolve_labels(project, params) }
- it { self.instance_exec(&test) }
+ specify { expect(subject).to match_array(instance_exec(&expected_labels)) }
end
end
end
diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
index c5c368fc88f..7dd968d90a8 100644
--- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb
+++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
@@ -69,7 +69,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
it 'batch-resolves by target project full path and IIDS', :request_store do
result = batch_sync(max_queries: queries_per_project) do
- resolve_mr(project, iids: [iid_1, iid_2])
+ resolve_mr(project, iids: [iid_1, iid_2]).to_a
end
expect(result).to contain_exactly(merge_request_1, merge_request_2)
diff --git a/spec/graphql/resolvers/release_milestones_resolver_spec.rb b/spec/graphql/resolvers/release_milestones_resolver_spec.rb
index f05069998d0..a5a523859f9 100644
--- a/spec/graphql/resolvers/release_milestones_resolver_spec.rb
+++ b/spec/graphql/resolvers/release_milestones_resolver_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Resolvers::ReleaseMilestonesResolver do
describe '#resolve' do
it "uses offset-pagination" do
- expect(resolved).to be_a(::Gitlab::Graphql::Pagination::OffsetPaginatedRelation)
+ expect(resolved).to be_a(::Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection)
end
it "includes the release's milestones in the returned OffsetActiveRecordRelationConnection" do
diff --git a/spec/lib/api/entities/user_spec.rb b/spec/lib/api/entities/user_spec.rb
index e35deeb6263..0deaf476977 100644
--- a/spec/lib/api/entities/user_spec.rb
+++ b/spec/lib/api/entities/user_spec.rb
@@ -35,4 +35,22 @@ RSpec.describe API::Entities::User do
expect(subject[:bot]).to eq(true)
end
end
+
+ context 'with project bot user' do
+ let(:user) { create(:user, :project_bot) }
+
+ context 'when the requester is not an admin' do
+ it 'does not expose project bot user name' do
+ expect(subject).not_to include(:name)
+ end
+ end
+
+ context 'when the requester is an admin' do
+ let(:current_user) { create(:user, :admin) }
+
+ it 'exposes project bot user name' do
+ expect(subject).to include(:name)
+ end
+ end
+ end
end
diff --git a/spec/support/graphql/resolver_factories.rb b/spec/support/graphql/resolver_factories.rb
new file mode 100644
index 00000000000..8188f17cc43
--- /dev/null
+++ b/spec/support/graphql/resolver_factories.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Graphql
+ module ResolverFactories
+ def new_resolver(resolved_value = 'Resolved value', method: :resolve)
+ case method
+ when :resolve
+ simple_resolver(resolved_value)
+ when :find_object
+ find_object_resolver(resolved_value)
+ else
+ raise "Cannot build a resolver for #{method}"
+ end
+ end
+
+ private
+
+ def simple_resolver(resolved_value = 'Resolved value')
+ Class.new(Resolvers::BaseResolver) do
+ define_method :resolve do |**_args|
+ resolved_value
+ end
+ end
+ end
+
+ def find_object_resolver(resolved_value = 'Found object')
+ Class.new(Resolvers::BaseResolver) do
+ include ::Gitlab::Graphql::Authorize::AuthorizeResource
+
+ def resolve(**args)
+ authorized_find!(**args)
+ end
+
+ define_method :find_object do |**_args|
+ resolved_value
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/features/members_table_helpers.rb b/spec/support/helpers/features/members_table_helpers.rb
index 4a0e218ed3e..80fd4bcf07a 100644
--- a/spec/support/helpers/features/members_table_helpers.rb
+++ b/spec/support/helpers/features/members_table_helpers.rb
@@ -41,6 +41,10 @@ module Spec
find_row(user.name)
end
+ def find_username_row(user)
+ find_row(user.username)
+ end
+
def find_invited_member_row(email)
find_row(email)
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 46d0c13dc18..0e05ca320ed 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -16,32 +16,127 @@ module GraphqlHelpers
underscored_field_name.to_s.camelize(:lower)
end
- # Run a loader's named resolver in a way that closely mimics the framework.
+ def self.deep_fieldnamerize(map)
+ map.to_h do |k, v|
+ [fieldnamerize(k), v.is_a?(Hash) ? deep_fieldnamerize(v) : v]
+ end
+ end
+
+ # Run this resolver exactly as it would be called in the framework. This
+ # includes all authorization hooks, all argument processing and all result
+ # wrapping.
+ # see: GraphqlHelpers#resolve_field
+ def resolve(
+ resolver_class, # [Class[<= BaseResolver]] The resolver at test.
+ obj: nil, # [Any] The BaseObject#object for the resolver (available as `#object` in the resolver).
+ args: {}, # [Hash] The arguments to the resolver (using client names).
+ ctx: {}, # [#to_h] The current context values.
+ schema: GitlabSchema, # [GraphQL::Schema] Schema to use during execution.
+ parent: :not_given, # A GraphQL query node to be passed as the `:parent` extra.
+ lookahead: :not_given # A GraphQL lookahead object to be passed as the `:lookahead` extra.
+ )
+ # All resolution goes through fields, so we need to create one here that
+ # uses our resolver. Thankfully, apart from the field name, resolvers
+ # contain all the configuration needed to define one.
+ field_options = resolver_class.field_options.merge(name: 'field_value')
+ field = ::Types::BaseField.new(**field_options)
+
+ # All mutations accept a single `:input` argument. Wrap arguments here.
+ # See the unwrapping below in GraphqlHelpers#resolve_field
+ args = { input: args } if resolver_class <= ::Mutations::BaseMutation && !args.key?(:input)
+
+ resolve_field(field, obj,
+ args: args,
+ ctx: ctx,
+ schema: schema,
+ object_type: resolver_parent,
+ extras: { parent: parent, lookahead: lookahead })
+ end
+
+ # Resolve the value of a field on an object.
+ #
+ # Use this method to test individual fields within type specs.
+ #
+ # e.g.
+ #
+ # issue = create(:issue)
+ # user = issue.author
+ # project = issue.project
#
- # First the `ready?` method is called. If it turns out that the resolver is not
- # ready, then the early return is returned instead.
+ # resolve_field(:author, issue, current_user: user, object_type: ::Types::IssueType)
+ # resolve_field(:issue, project, args: { iid: issue.iid }, current_user: user, object_type: ::Types::ProjectType)
#
- # Then the resolve method is called.
- def resolve(resolver_class, args: {}, lookahead: :not_given, parent: :not_given, **resolver_args)
- args = aliased_args(resolver_class, args)
- args[:parent] = parent unless parent == :not_given
- args[:lookahead] = lookahead unless lookahead == :not_given
- resolver = resolver_instance(resolver_class, **resolver_args)
- ready, early_return = sync_all { resolver.ready?(**args) }
+ # The `object_type` defaults to the `described_class`, so when called from type specs,
+ # the above can be written as:
+ #
+ # # In project_type_spec.rb
+ # resolve_field(:author, issue, current_user: user)
+ #
+ # # In issue_type_spec.rb
+ # resolve_field(:issue, project, args: { iid: issue.iid }, current_user: user)
+ #
+ # NB: Arguments are passed from the client's perspective. If there is an argument
+ # `foo` aliased as `bar`, then we would pass `args: { bar: the_value }`, and
+ # types are checked before resolution.
+ def resolve_field(
+ field, # An instance of `BaseField`, or the name of a field on the current described_class
+ object, # The current object of the `BaseObject` this field 'belongs' to
+ args: {}, # Field arguments (keys will be fieldnamerized)
+ ctx: {}, # Context values (important ones are :current_user)
+ extras: {}, # Stub values for field extras (parent and lookahead)
+ current_user: :not_given, # The current user (specified explicitly, overrides ctx[:current_user])
+ schema: GitlabSchema, # A specific schema instance
+ object_type: described_class # The `BaseObject` type this field belongs to
+ )
+ field = to_base_field(field, object_type)
+ ctx[:current_user] = current_user unless current_user == :not_given
+ query = GraphQL::Query.new(schema, context: ctx.to_h)
+ extras[:lookahead] = negative_lookahead if extras[:lookahead] == :not_given && field.extras.include?(:lookahead)
+
+ query_ctx = query.context
+
+ mock_extras(query_ctx, **extras)
+
+ parent = object_type.authorized_new(object, query_ctx)
+ raise UnauthorizedObject unless parent
+
+ # TODO: This will need to change when we move to the interpreter:
+ # At that point, arguments will be a plain ruby hash rather than
+ # an Arguments object
+ # see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27536
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/210556
+ arguments = field.to_graphql.arguments_class.new(
+ GraphqlHelpers.deep_fieldnamerize(args),
+ context: query_ctx,
+ defaults_used: []
+ )
+
+ # we enable the request store so we can track gitaly calls.
+ ::Gitlab::WithRequestStore.with_request_store do
+ # TODO: This will need to change when we move to the interpreter - at that
+ # point we will call `field#resolve`
- return early_return unless ready
+ # Unwrap the arguments to mutations. This pairs with the wrapping in GraphqlHelpers#resolve
+ # If arguments are not wrapped first, then arguments processing will raise.
+ # If arguments are not unwrapped here, then the resolve method of the mutation will raise argument errors.
+ arguments = arguments.to_kwargs[:input] if field.resolver && field.resolver <= ::Mutations::BaseMutation
- resolver.resolve(**args)
+ field.resolve_field(parent, arguments, query_ctx)
+ end
end
- # TODO: Remove this method entirely when GraphqlHelpers uses real resolve_field
- # see: https://gitlab.com/gitlab-org/gitlab/-/issues/287791
- def aliased_args(resolver, args)
- definitions = resolver.arguments
+ def mock_extras(context, parent: :not_given, lookahead: :not_given)
+ allow(context).to receive(:parent).and_return(parent) unless parent == :not_given
+ allow(context).to receive(:lookahead).and_return(lookahead) unless lookahead == :not_given
+ end
- args.transform_keys do |k|
- definitions[GraphqlHelpers.fieldnamerize(k)]&.keyword || k
- end
+ # a synthetic BaseObject type to be used in resolver specs. See `GraphqlHelpers#resolve`
+ def resolver_parent
+ @resolver_parent ||= fresh_object_type('ResolverParent')
+ end
+
+ def fresh_object_type(name = 'Object')
+ Class.new(::Types::BaseObject) { graphql_name name }
end
def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema)
@@ -124,9 +219,9 @@ module GraphqlHelpers
lazy_vals.is_a?(Array) ? lazy_vals.map { |val| sync(val) } : sync(lazy_vals)
end
- def graphql_query_for(name, attributes = {}, fields = nil)
+ def graphql_query_for(name, args = {}, selection = nil)
type = GitlabSchema.types['Query'].fields[GraphqlHelpers.fieldnamerize(name)]&.type
- wrap_query(query_graphql_field(name, attributes, fields, type))
+ wrap_query(query_graphql_field(name, args, selection, type))
end
def wrap_query(query)
@@ -171,25 +266,6 @@ module GraphqlHelpers
::Gitlab::Utils::MergeHash.merge(Array.wrap(variables).map(&:to_h)).to_json
end
- def resolve_field(name, object, args = {}, current_user: nil)
- q = GraphQL::Query.new(GitlabSchema)
- context = GraphQL::Query::Context.new(query: q, object: object, values: { current_user: current_user })
- allow(context).to receive(:parent).and_return(nil)
- field = described_class.fields.fetch(GraphqlHelpers.fieldnamerize(name))
- instance = described_class.authorized_new(object, context)
- raise UnauthorizedObject unless instance
-
- field.resolve_field(instance, args, context)
- end
-
- def simple_resolver(resolved_value = 'Resolved value')
- Class.new(Resolvers::BaseResolver) do
- define_method :resolve do |**_args|
- resolved_value
- end
- end
- end
-
# Recursively convert a Hash with Ruby-style keys to GraphQL fieldname-style keys
#
# prepare_input_for_mutation({ 'my_key' => 1 })
@@ -558,24 +634,26 @@ module GraphqlHelpers
end
end
- def execute_query(query_type)
- schema = Class.new(GraphQL::Schema) do
- use GraphQL::Pagination::Connections
- use Gitlab::Graphql::Authorize
- use Gitlab::Graphql::Pagination::Connections
-
- lazy_resolve ::Gitlab::Graphql::Lazy, :force
-
- query(query_type)
- end
+ # assumes query_string to be let-bound in the current context
+ def execute_query(query_type, schema: empty_schema, graphql: query_string)
+ schema.query(query_type)
schema.execute(
- query_string,
+ graphql,
context: { current_user: user },
variables: {}
)
end
+ def empty_schema
+ Class.new(GraphQL::Schema) do
+ use GraphQL::Pagination::Connections
+ use Gitlab::Graphql::Pagination::Connections
+
+ lazy_resolve ::Gitlab::Graphql::Lazy, :force
+ end
+ end
+
# A lookahead that selects everything
def positive_lookahead
double(selects?: true).tap do |selection|
@@ -589,6 +667,23 @@ module GraphqlHelpers
allow(selection).to receive(:selection).and_return(selection)
end
end
+
+ private
+
+ def to_base_field(name_or_field, object_type)
+ case name_or_field
+ when ::Types::BaseField
+ name_or_field
+ else
+ field_by_name(name_or_field, object_type)
+ end
+ end
+
+ def field_by_name(name, object_type)
+ name = ::GraphqlHelpers.fieldnamerize(name)
+
+ object_type.fields[name] || (raise ArgumentError, "Unknown field #{name} for #{described_class.graphql_name}")
+ end
end
# This warms our schema, doing this as part of loading the helpers to avoid
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 72033ecf187..4411c91d479 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
@@ -10,7 +10,7 @@ RSpec.shared_examples 'it uploads and commit a new text file' do
wait_for_requests
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
fill_in(:commit_message, with: 'New commit message')
@@ -42,7 +42,7 @@ RSpec.shared_examples 'it uploads and commit a new image file' do
wait_for_requests
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
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
expect(page).to have_content(fork_message)
- wait_for_all_requests
-
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)
+ 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')
@@ -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')
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