Commit 33b0c3e0 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'id-remove-fork-project-form-ff' into 'master'

Remove fork_project_form feature flag

See merge request gitlab-org/gitlab!77181
parents fdbb11ea ecef867f
...@@ -10,38 +10,6 @@ export default { ...@@ -10,38 +10,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
endpoint: {
type: String,
required: true,
},
projectFullPath: {
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,
},
restrictedVisibilityLevels: {
type: Array,
required: true,
},
}, },
}; };
</script> </script>
...@@ -62,16 +30,7 @@ export default { ...@@ -62,16 +30,7 @@ export default {
</p> </p>
</div> </div>
<div class="col-lg-9"> <div class="col-lg-9">
<fork-form <fork-form />
:endpoint="endpoint"
:project-full-path="projectFullPath"
:project-id="projectId"
:project-name="projectName"
:project-path="projectPath"
:project-description="projectDescription"
:project-visibility="projectVisibility"
:restricted-visibility-levels="restrictedVisibilityLevels"
/>
</div> </div>
</div> </div>
</template> </template>
...@@ -72,40 +72,29 @@ export default { ...@@ -72,40 +72,29 @@ export default {
visibilityHelpPath: { visibilityHelpPath: {
default: '', default: '',
}, },
},
props: {
endpoint: { endpoint: {
type: String, default: '',
required: true,
}, },
projectFullPath: { projectFullPath: {
type: String, default: '',
required: true,
}, },
projectId: { projectId: {
type: String, default: '',
required: true,
}, },
projectName: { projectName: {
type: String, default: '',
required: true,
}, },
projectPath: { projectPath: {
type: String, default: '',
required: true,
}, },
projectDescription: { projectDescription: {
type: String,
required: false,
default: '', default: '',
}, },
projectVisibility: { projectVisibility: {
type: String, default: '',
required: true,
}, },
restrictedVisibilityLevels: { restrictedVisibilityLevels: {
type: Array, default: [],
required: true,
}, },
}, },
data() { data() {
......
<script>
import { GlTabs, GlTab, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import ForkGroupsListItem from './fork_groups_list_item.vue';
export default {
components: {
GlTabs,
GlTab,
GlLoadingIcon,
GlSearchBoxByType,
ForkGroupsListItem,
},
props: {
endpoint: {
type: String,
required: true,
},
},
data() {
return {
namespaces: null,
filter: '',
};
},
computed: {
filteredNamespaces() {
return this.namespaces.filter((n) =>
n.name.toLowerCase().includes(this.filter.toLowerCase()),
);
},
},
mounted() {
this.loadGroups();
},
methods: {
loadGroups() {
axios
.get(this.endpoint)
.then((response) => {
this.namespaces = response.data.namespaces;
})
.catch(() =>
createFlash({
message: __('There was a problem fetching groups.'),
}),
);
},
},
i18n: {
searchPlaceholder: __('Search by name'),
},
};
</script>
<template>
<gl-tabs class="fork-groups">
<gl-tab :title="__('Groups and subgroups')">
<gl-loading-icon v-if="!namespaces" size="md" class="gl-mt-3" />
<template v-else-if="namespaces.length === 0">
<div class="gl-text-center">
<div class="h5">{{ __('No available groups to fork the project.') }}</div>
<p class="gl-mt-5">
{{ __('You must have permission to create a project in a group before forking.') }}
</p>
</div>
</template>
<div v-else-if="filteredNamespaces.length === 0" class="gl-text-center gl-mt-3">
{{ s__('GroupsTree|No groups matched your search') }}
</div>
<ul v-else class="groups-list group-list-tree">
<fork-groups-list-item
v-for="(namespace, index) in filteredNamespaces"
:key="index"
:group="namespace"
/>
</ul>
</gl-tab>
<template #tabs-end>
<gl-search-box-by-type
v-if="namespaces && namespaces.length"
v-model="filter"
:placeholder="$options.i18n.searchPlaceholder"
class="gl-align-self-center gl-ml-auto fork-filtered-search"
data-qa-selector="fork_groups_list_search_field"
/>
</template>
</gl-tabs>
</template>
<script>
import {
GlLink,
GlButton,
GlIcon,
GlAvatar,
GlTooltipDirective,
GlTooltip,
GlBadge,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/groups/constants';
import csrf from '~/lib/utils/csrf';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
export default {
components: {
GlIcon,
GlAvatar,
GlBadge,
GlButton,
GlTooltip,
GlLink,
UserAccessRoleBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
SafeHtml,
},
props: {
group: {
type: Object,
required: true,
},
},
data() {
return { namespaces: null, isForking: false };
},
computed: {
rowClass() {
return {
'has-description': this.group.description,
'being-removed': this.isGroupPendingRemoval,
};
},
isGroupPendingRemoval() {
return this.group.marked_for_deletion;
},
hasForkedProject() {
return Boolean(this.group.forked_project_path);
},
visibilityIcon() {
return VISIBILITY_TYPE_ICON[this.group.visibility];
},
visibilityTooltip() {
return GROUP_VISIBILITY_TYPE[this.group.visibility];
},
isSelectButtonDisabled() {
return !this.group.can_create_project;
},
},
methods: {
fork() {
this.isForking = true;
this.$refs.form.submit();
},
},
csrf,
};
</script>
<template>
<li :class="rowClass" class="group-row">
<div class="group-row-contents gl-display-flex gl-align-items-center gl-py-3 gl-pr-5">
<div
class="folder-toggle-wrap gl-mr-3 gl-display-flex gl-align-items-center gl-text-gray-500"
>
<gl-icon name="folder-o" />
</div>
<gl-link
:href="group.relative_path"
class="gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3"
>
<gl-avatar :size="32" shape="rect" :entity-name="group.name" :src="group.avatarUrl" />
</gl-link>
<div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center">
<div class="gl-min-w-0 gl-flex-grow-1 flex-shrink-1">
<div class="title gl-display-flex gl-align-items-center gl-flex-wrap gl-mr-3">
<gl-link :href="group.relative_path" class="gl-mt-3 gl-mr-3 gl-text-gray-900!">
{{ group.full_name }}
</gl-link>
<gl-icon
v-gl-tooltip.hover.bottom
class="gl-display-inline-flex gl-mt-3 gl-mr-3 gl-text-gray-500"
:name="visibilityIcon"
:title="visibilityTooltip"
/>
<gl-badge
v-if="isGroupPendingRemoval"
variant="warning"
class="gl-display-none gl-sm-display-flex gl-mt-3 gl-mr-1"
>{{ __('pending deletion') }}</gl-badge
>
<user-access-role-badge v-if="group.permission" class="gl-mt-3">
{{ group.permission }}
</user-access-role-badge>
</div>
<div v-if="group.description" class="description gl-line-height-20">
<span v-safe-html="group.markdown_description"> </span>
</div>
</div>
<div class="gl-display-flex gl-flex-shrink-0">
<gl-button
v-if="hasForkedProject"
class="gl-h-7 gl-text-decoration-none!"
:href="group.forked_project_path"
>{{ __('Go to fork') }}</gl-button
>
<template v-else>
<div ref="selectButtonWrapper">
<form ref="form" method="POST" :action="group.fork_path">
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
<gl-button
type="submit"
class="gl-h-7"
:data-qa-name="group.full_name"
category="secondary"
variant="success"
:disabled="isSelectButtonDisabled"
:loading="isForking"
@click="fork"
>{{ __('Select') }}</gl-button
>
</form>
</div>
<gl-tooltip v-if="isSelectButtonDisabled" :target="() => $refs.selectButtonWrapper">
{{
__('You must have permission to create a project in a namespace before forking.')
}}
</gl-tooltip>
</template>
</div>
</div>
</div>
</li>
</template>
import Vue from 'vue'; import Vue from 'vue';
import App from './components/app.vue'; import App from './components/app.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');
if (gon.features.forkProjectForm) { const {
const {
forkIllustration, forkIllustration,
endpoint, endpoint,
newGroupPath, newGroupPath,
...@@ -17,23 +15,16 @@ if (gon.features.forkProjectForm) { ...@@ -17,23 +15,16 @@ if (gon.features.forkProjectForm) {
projectDescription, projectDescription,
projectVisibility, projectVisibility,
restrictedVisibilityLevels, restrictedVisibilityLevels,
} = mountElement.dataset; } = mountElement.dataset;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el: mountElement, el: mountElement,
provide: { provide: {
newGroupPath, newGroupPath,
visibilityHelpPath, visibilityHelpPath,
},
render(h) {
return h(App, {
props: {
forkIllustration,
endpoint, endpoint,
newGroupPath,
projectFullPath, projectFullPath,
visibilityHelpPath,
projectId, projectId,
projectName, projectName,
projectPath, projectPath,
...@@ -41,21 +32,11 @@ if (gon.features.forkProjectForm) { ...@@ -41,21 +32,11 @@ if (gon.features.forkProjectForm) {
projectVisibility, projectVisibility,
restrictedVisibilityLevels: JSON.parse(restrictedVisibilityLevels), restrictedVisibilityLevels: JSON.parse(restrictedVisibilityLevels),
}, },
});
},
});
} else {
const { endpoint } = mountElement.dataset;
// eslint-disable-next-line no-new
new Vue({
el: mountElement,
render(h) { render(h) {
return h(ForkGroupsList, { return h(App, {
props: { props: {
endpoint, forkIllustration,
}, },
}); });
}, },
}); });
}
...@@ -17,10 +17,6 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -17,10 +17,6 @@ class Projects::ForksController < Projects::ApplicationController
feature_category :source_code_management feature_category :source_code_management
urgency :low, [:index] urgency :low, [:index]
before_action do
push_frontend_feature_flag(:fork_project_form, @project, default_enabled: :yaml)
end
def index def index
@sort = forks_params[:sort] @sort = forks_params[:sort]
...@@ -54,9 +50,7 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -54,9 +50,7 @@ class Projects::ForksController < Projects::ApplicationController
format.json do format.json do
namespaces = load_namespaces_with_associations - [project.namespace] namespaces = load_namespaces_with_associations - [project.namespace]
namespaces = [current_user.namespace] + namespaces if namespaces = [current_user.namespace] + namespaces if can_fork_to?(current_user.namespace)
Feature.enabled?(:fork_project_form, project, default_enabled: :yaml) &&
can_fork_to?(current_user.namespace)
render json: { render json: {
namespaces: ForkNamespaceSerializer.new.represent( namespaces: ForkNamespaceSerializer.new.represent(
......
...@@ -30,14 +30,6 @@ class ForkNamespaceEntity < Grape::Entity ...@@ -30,14 +30,6 @@ class ForkNamespaceEntity < Grape::Entity
markdown_description(namespace) markdown_description(namespace)
end end
expose :can_create_project do |namespace, options|
if Feature.enabled?(:fork_project_form, options[:project], default_enabled: :yaml)
true
else
options[:current_user].can?(:create_projects, namespace)
end
end
private private
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
......
- avatar = namespace_icon(namespace, 100)
- can_create_project = current_user.can?(:create_projects, namespace)
.bordered-box.fork-thumbnail.text-center.gl-m-3.gl-pb-5{ class: ("disabled" unless can_create_project) }
- if /no_((\w*)_)*avatar/.match(avatar)
= group_icon(namespace, class: "avatar rect-avatar s100 identicon mx-auto")
- else
.avatar-container.s100.mx-auto.gl-mt-5
= image_tag(avatar, class: "avatar s100")
%h5.gl-mt-3
= namespace.human_name
- if forked_project = namespace.find_fork_of(@project)
= link_to _("Go to project"), project_path(forked_project), class: "btn gl-button btn-default"
- else
%div{ class: ('has-tooltip' unless can_create_project),
title: (_('You have reached your project limit') unless can_create_project) }
= link_to _("Select"), project_forks_path(@project, namespace_key: namespace.id),
data: { qa_selector: 'fork_namespace_button', qa_name: namespace.human_name },
method: "POST",
class: ["btn gl-button btn-confirm", ("disabled" unless can_create_project)]
- page_title s_("ForkProject|Fork project") - page_title s_("ForkProject|Fork project")
- if Feature.enabled?(:fork_project_form, @project, default_enabled: :yaml) #fork-groups-mount-element{ data: { fork_illustration: image_path('illustrations/project-create-new-sm.svg'),
#fork-groups-mount-element{ data: { fork_illustration: image_path('illustrations/project-create-new-sm.svg'),
endpoint: new_project_fork_path(@project, format: :json), endpoint: new_project_fork_path(@project, format: :json),
new_group_path: new_group_path, new_group_path: new_group_path,
project_full_path: project_path(@project), project_full_path: project_path(@project),
...@@ -12,19 +11,3 @@ ...@@ -12,19 +11,3 @@
project_description: @project.description, project_description: @project.description,
project_visibility: @project.visibility, project_visibility: @project.visibility,
restricted_visibility_levels: Gitlab::CurrentSettings.restricted_visibility_levels.to_json } } restricted_visibility_levels: Gitlab::CurrentSettings.restricted_visibility_levels.to_json } }
- else
.row.gl-mt-3
.col-lg-3
%h4.gl-mt-0
= s_("ForkProject|Fork project")
%p
= s_("ForkProject|A fork is a copy of a project.")
%br
= s_('ForkProject|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
= s_("ForkProject|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) } }
---
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: true
...@@ -18,36 +18,24 @@ submit them through a merge request to the repository you don't have access to. ...@@ -18,36 +18,24 @@ submit them through a merge request to the repository you don't have access to.
## Creating a fork ## Creating a fork
To fork an existing project in GitLab: > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15013) a new form in GitLab 13.11 [with a flag](../../../user/feature_flags.md) named `fork_project_form`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77181) in GitLab 14.8. Feature flag `fork_project_form` removed.
1. On the project's home page, in the top right, click **{fork}** **Fork**.
![Fork button](img/forking_workflow_fork_button_v13_10.png)
1. Select the project to fork to:
- Recommended method. Below **Select a namespace to fork the project**, identify To fork an existing project in GitLab:
the project you want to fork to, and click **Select**. Only namespaces where you have
at least the Developer role for are shown.
![Choose namespace](img/forking_workflow_choose_namespace_v13_10.png)
- Experimental method. If your GitLab administrator has
enabled the experimental fork project form, read
[Create a fork with the fork project form](#create-a-fork-with-the-fork-project-form).
Only namespaces where you have at least the Developer role for are shown.
NOTE:
The project path must be unique in the namespace.
GitLab creates your fork, and redirects you to the project page for your new fork. 1. On the project's home page, in the top right, select **{fork}** **Fork**:
The permissions you have in the namespace are your permissions in the fork. ![Fork this project](img/forking_workflow_fork_button_v13_10.png)
1. Optional. Edit the **Project name**.
1. For **Project URL**, select the [namespace](../../group/index.md#namespaces)
your fork should belong to.
1. Add a **Project slug**. This value becomes part of the URL to your fork.
It must be unique in the namespace.
1. Optional. Add a **Project description**.
1. Select the **Visibility level** for your fork. For more information about
visibility levels, read [Project and group visibility](../../../public_access/public_access.md).
1. Select **Fork project**.
WARNING: GitLab creates your fork, and redirects you to the new fork's page.
When a public project with the repository feature set to **Members Only**
is forked, the repository is public in the fork. The owner
of the fork must manually change the visibility. Issue
[#36662](https://gitlab.com/gitlab-org/gitlab/-/issues/36662) exists for this issue.
## Repository mirroring ## Repository mirroring
...@@ -81,24 +69,3 @@ changes are added to the repository and branch you're merging into. ...@@ -81,24 +69,3 @@ changes are added to the repository and branch you're merging into.
## Removing a fork relationship ## Removing a fork relationship
You can unlink your fork from its upstream project in the [advanced settings](../settings/index.md#removing-a-fork-relationship). You can unlink your fork from its upstream project in the [advanced settings](../settings/index.md#removing-a-fork-relationship).
## Create a fork with the fork project form **(FREE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15013) in GitLab 13.11 [with a flag](../../../administration/feature_flags.md) named `fork_project_form`. Disabled by default.
> - [Enabled on self-managed and GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64967) in GitLab 13.8.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../../../administration/feature_flags.md) named `fork_project_form`.
On GitLab.com, this feature is available.
This version of the fork project form is experimental:
![Choose namespace](img/fork_form_v13_10.png)
To use it, follow the instructions at [Creating a fork](#creating-a-fork) and provide:
- The project name.
- The project URL.
- The project slug.
- Optional. The project description.
- The visibility level for your fork.
...@@ -15814,9 +15814,6 @@ msgstr "" ...@@ -15814,9 +15814,6 @@ msgstr ""
msgid "ForkProject|Select a namespace" msgid "ForkProject|Select a namespace"
msgstr "" msgstr ""
msgid "ForkProject|Select a namespace to fork the project"
msgstr ""
msgid "ForkProject|The project can be accessed by any logged in user." msgid "ForkProject|The project can be accessed by any logged in user."
msgstr "" msgstr ""
...@@ -16864,9 +16861,6 @@ msgstr "" ...@@ -16864,9 +16861,6 @@ msgstr ""
msgid "Go to find file" msgid "Go to find file"
msgstr "" msgstr ""
msgid "Go to fork"
msgstr ""
msgid "Go to issue boards" msgid "Go to issue boards"
msgstr "" msgstr ""
...@@ -17668,9 +17662,6 @@ msgstr "" ...@@ -17668,9 +17662,6 @@ msgstr ""
msgid "Groups and projects" msgid "Groups and projects"
msgstr "" msgstr ""
msgid "Groups and subgroups"
msgstr ""
msgid "Groups are a great way to organize projects and people." msgid "Groups are a great way to organize projects and people."
msgstr "" msgstr ""
...@@ -24561,9 +24552,6 @@ msgstr "" ...@@ -24561,9 +24552,6 @@ msgstr ""
msgid "No available branches" msgid "No available branches"
msgstr "" msgstr ""
msgid "No available groups to fork the project."
msgstr ""
msgid "No branches found" msgid "No branches found"
msgstr "" msgstr ""
...@@ -42073,9 +42061,6 @@ msgstr "" ...@@ -42073,9 +42061,6 @@ msgstr ""
msgid "You have not added any approvers. Start by adding users or groups." msgid "You have not added any approvers. Start by adding users or groups."
msgstr "" msgstr ""
msgid "You have reached your project limit"
msgstr ""
msgid "You have set up 2FA for your account! If you lose access to your 2FA device, you can use your recovery codes to access your account. Alternatively, if you upload an SSH key, you can %{anchorOpen}use that key to generate additional recovery codes%{anchorClose}." msgid "You have set up 2FA for your account! If you lose access to your 2FA device, you can use your recovery codes to access your account. Alternatively, if you upload an SSH key, you can %{anchorOpen}use that key to generate additional recovery codes%{anchorClose}."
msgstr "" msgstr ""
...@@ -42106,12 +42091,6 @@ msgstr "" ...@@ -42106,12 +42091,6 @@ msgstr ""
msgid "You must have maintainer access to force delete a lock" msgid "You must have maintainer access to force delete a lock"
msgstr "" msgstr ""
msgid "You must have permission to create a project in a group before forking."
msgstr ""
msgid "You must have permission to create a project in a namespace before forking."
msgstr ""
msgid "You must provide a valid current password" msgid "You must provide a valid current password"
msgstr "" msgstr ""
......
...@@ -5,10 +5,6 @@ module QA ...@@ -5,10 +5,6 @@ module QA
module Project module Project
module Fork module Fork
class New < Page::Base class New < Page::Base
view 'app/views/projects/forks/_fork_button.html.haml' do
element :fork_namespace_button
end
view 'app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue' do view 'app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue' do
element :fork_namespace_dropdown element :fork_namespace_dropdown
element :fork_project_button element :fork_project_button
...@@ -16,14 +12,10 @@ module QA ...@@ -16,14 +12,10 @@ module QA
end end
def fork_project(namespace = Runtime::Namespace.path) def fork_project(namespace = Runtime::Namespace.path)
if has_element?(:fork_namespace_button, wait: 0)
click_element(:fork_namespace_button, name: namespace)
else
select_element(:fork_namespace_dropdown, namespace) select_element(:fork_namespace_dropdown, namespace)
click_element(:fork_privacy_button, privacy_level: 'public') click_element(:fork_privacy_button, privacy_level: 'public')
click_element(:fork_project_button) click_element(:fork_project_button)
end end
end
def fork_namespace_dropdown_values def fork_namespace_dropdown_values
find_element(:fork_namespace_dropdown).all(:option).map { |option| option.text.tr("\n", '').strip } find_element(:fork_namespace_dropdown).all(:option).map { |option| option.text.tr("\n", '').strip }
......
...@@ -199,15 +199,6 @@ RSpec.describe Projects::ForksController do ...@@ -199,15 +199,6 @@ RSpec.describe Projects::ForksController do
expect(json_response['namespaces'][1]['id']).to eq(group.id) expect(json_response['namespaces'][1]['id']).to eq(group.id)
end end
it 'responds with group only when fork_project_form feature flag is disabled' do
stub_feature_flags(fork_project_form: false)
do_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['namespaces'].length).to eq(1)
expect(json_response['namespaces'][0]['id']).to eq(group.id)
end
context 'N+1 queries' do context 'N+1 queries' do
before do before do
create(:fork_network, root_project: project) create(:fork_network, root_project: project)
......
...@@ -164,199 +164,4 @@ RSpec.describe 'Project fork' do ...@@ -164,199 +164,4 @@ RSpec.describe 'Project fork' do
end end
end end
end end
context 'with fork_project_form feature flag disabled' do
before do
stub_feature_flags(fork_project_form: false)
sign_in(user)
end
it_behaves_like 'fork button on project page'
context 'user has exceeded personal project limit' do
before do
user.update!(projects_limit: 0)
end
context 'with a group to fork to' do
let!(:group) { create(:group).tap { |group| group.add_owner(user) } }
it 'allows user to fork only to the group on fork page', :js do
visit new_project_fork_path(project)
to_personal_namespace = find('[data-qa-selector=fork_namespace_button].disabled') # rubocop:disable QA/SelectorUsage
to_group = find(".fork-groups button[data-qa-name=#{group.name}]") # rubocop:disable QA/SelectorUsage
expect(to_personal_namespace).not_to be_nil
expect(to_group).not_to be_disabled
end
end
end
it_behaves_like 'create fork page', ' Select a namespace to fork the project '
it 'forks the project', :sidekiq_might_not_need_inline do
visit project_path(project)
click_link 'Fork'
page.within '.fork-thumbnail-container' do
click_link 'Select'
end
expect(page).to have_content 'Forked from'
visit project_path(project)
expect(page).to have_content(/new merge request/i)
page.within '.nav-sidebar' do
first(:link, 'Merge requests').click
end
expect(page).to have_content(/new merge request/i)
page.within '#content-body' do
click_link('New merge request')
end
expect(current_path).to have_content(/#{user.namespace.path}/i)
end
it 'shows avatars when Gravatar is disabled' do
stub_application_setting(gravatar_enabled: false)
visit project_path(project)
click_link 'Fork'
page.within('.fork-thumbnail-container') do
expect(page).to have_css('span.identicon')
end
end
it 'shows the forked project on the list' do
visit project_path(project)
click_link 'Fork'
page.within '.fork-thumbnail-container' do
click_link 'Select'
end
visit project_forks_path(project)
forked_project = user.fork_of(project.reload)
page.within('.js-projects-list-holder') do
expect(page).to have_content("#{forked_project.namespace.human_name} / #{forked_project.name}")
end
forked_project.update!(path: 'test-crappy-path')
visit project_forks_path(project)
page.within('.js-projects-list-holder') do
expect(page).to have_content("#{forked_project.namespace.human_name} / #{forked_project.name}")
end
end
context 'when the project is private' do
let(:project) { create(:project, :repository) }
let(:another_user) { create(:user, name: 'Mike') }
before do
project.add_reporter(user)
project.add_reporter(another_user)
end
it 'renders private forks of the project' do
visit project_path(project)
another_project_fork = Projects::ForkService.new(project, another_user).execute
click_link 'Fork'
page.within '.fork-thumbnail-container' do
click_link 'Select'
end
visit project_forks_path(project)
page.within('.js-projects-list-holder') do
user_project_fork = user.fork_of(project.reload)
expect(page).to have_content("#{user_project_fork.namespace.human_name} / #{user_project_fork.name}")
end
expect(page).not_to have_content("#{another_project_fork.namespace.human_name} / #{another_project_fork.name}")
end
end
context 'when the user already forked the project' do
before do
create(:project, :repository, name: project.name, namespace: user.namespace)
end
it 'renders error' do
visit project_path(project)
click_link 'Fork'
page.within '.fork-thumbnail-container' do
click_link 'Select'
end
expect(page).to have_content "Name has already been taken"
end
end
context 'maintainer in group' do
let(:group) { create(:group) }
before do
group.add_maintainer(user)
end
it 'allows user to fork project to group or to user namespace', :js do
visit project_path(project)
wait_for_requests
expect(page).not_to have_css('a.disabled', text: 'Fork')
click_link 'Fork'
expect(page).to have_css('.fork-thumbnail')
expect(page).to have_css('.group-row')
expect(page).not_to have_css('.fork-thumbnail.disabled')
end
it 'allows user to fork project to group and not user when exceeded project limit', :js do
user.projects_limit = 0
user.save!
visit project_path(project)
wait_for_requests
expect(page).not_to have_css('a.disabled', text: 'Fork')
click_link 'Fork'
expect(page).to have_css('.fork-thumbnail.disabled')
expect(page).to have_css('.group-row')
end
it 'links to the fork if the project was already forked within that namespace', :sidekiq_might_not_need_inline, :js do
forked_project = fork_project(project, user, namespace: group, repository: true)
visit new_project_fork_path(project)
wait_for_requests
expect(page).to have_css('.group-row a.btn', text: 'Go to fork')
click_link 'Go to fork'
expect(current_path).to eq(project_path(forked_project))
end
end
end
end end
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import App from '~/pages/projects/forks/new/components/app.vue'; import App from '~/pages/projects/forks/new/components/app.vue';
import ForkForm from '~/pages/projects/forks/new/components/fork_form.vue';
describe('App component', () => { describe('App component', () => {
let wrapper; let wrapper;
const DEFAULT_PROPS = { const DEFAULT_PROPS = {
forkIllustration: 'illustrations/project-create-new-sm.svg', forkIllustration: 'illustrations/project-create-new-sm.svg',
endpoint: '/some/project-full-path/-/forks/new.json',
projectFullPath: '/some/project-full-path',
projectId: '10',
projectName: 'Project Name',
projectPath: 'project-name',
projectDescription: 'some project description',
projectVisibility: 'private',
restrictedVisibilityLevels: [],
}; };
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
...@@ -37,7 +30,7 @@ describe('App component', () => { ...@@ -37,7 +30,7 @@ describe('App component', () => {
expect(wrapper.find('img').attributes('src')).toBe('illustrations/project-create-new-sm.svg'); expect(wrapper.find('img').attributes('src')).toBe('illustrations/project-create-new-sm.svg');
}); });
it('renders ForkForm component with prop', () => { it('renders ForkForm component', () => {
expect(wrapper.props()).toEqual(expect.objectContaining(DEFAULT_PROPS)); expect(wrapper.findComponent(ForkForm).exists()).toBe(true);
}); });
}); });
...@@ -40,7 +40,9 @@ describe('ForkForm component', () => { ...@@ -40,7 +40,9 @@ describe('ForkForm component', () => {
}, },
]; ];
const DEFAULT_PROPS = { const DEFAULT_PROVIDE = {
newGroupPath: 'some/groups/path',
visibilityHelpPath: 'some/visibility/help/path',
endpoint: '/some/project-full-path/-/forks/new.json', endpoint: '/some/project-full-path/-/forks/new.json',
projectFullPath: '/some/project-full-path', projectFullPath: '/some/project-full-path',
projectId: '10', projectId: '10',
...@@ -52,18 +54,14 @@ describe('ForkForm component', () => { ...@@ -52,18 +54,14 @@ describe('ForkForm component', () => {
}; };
const mockGetRequest = (data = {}, statusCode = httpStatus.OK) => { const mockGetRequest = (data = {}, statusCode = httpStatus.OK) => {
axiosMock.onGet(DEFAULT_PROPS.endpoint).replyOnce(statusCode, data); axiosMock.onGet(DEFAULT_PROVIDE.endpoint).replyOnce(statusCode, data);
}; };
const createComponentFactory = (mountFn) => (props = {}, data = {}) => { const createComponentFactory = (mountFn) => (provide = {}, data = {}) => {
wrapper = mountFn(ForkForm, { wrapper = mountFn(ForkForm, {
provide: { provide: {
newGroupPath: 'some/groups/path', ...DEFAULT_PROVIDE,
visibilityHelpPath: 'some/visibility/help/path', ...provide,
},
propsData: {
...DEFAULT_PROPS,
...props,
}, },
data() { data() {
return { return {
...@@ -111,7 +109,7 @@ describe('ForkForm component', () => { ...@@ -111,7 +109,7 @@ describe('ForkForm component', () => {
mockGetRequest(); mockGetRequest();
createComponent(); createComponent();
const { projectFullPath } = DEFAULT_PROPS; const { projectFullPath } = DEFAULT_PROVIDE;
const cancelButton = wrapper.find('[data-testid="cancel-button"]'); const cancelButton = wrapper.find('[data-testid="cancel-button"]');
expect(cancelButton.attributes('href')).toBe(projectFullPath); expect(cancelButton.attributes('href')).toBe(projectFullPath);
...@@ -130,10 +128,10 @@ describe('ForkForm component', () => { ...@@ -130,10 +128,10 @@ describe('ForkForm component', () => {
mockGetRequest(); mockGetRequest();
createComponent(); createComponent();
expect(findForkNameInput().attributes('value')).toBe(DEFAULT_PROPS.projectName); expect(findForkNameInput().attributes('value')).toBe(DEFAULT_PROVIDE.projectName);
expect(findForkSlugInput().attributes('value')).toBe(DEFAULT_PROPS.projectPath); expect(findForkSlugInput().attributes('value')).toBe(DEFAULT_PROVIDE.projectPath);
expect(findForkDescriptionTextarea().attributes('value')).toBe( expect(findForkDescriptionTextarea().attributes('value')).toBe(
DEFAULT_PROPS.projectDescription, DEFAULT_PROVIDE.projectDescription,
); );
}); });
...@@ -164,7 +162,7 @@ describe('ForkForm component', () => { ...@@ -164,7 +162,7 @@ describe('ForkForm component', () => {
it('make GET request from endpoint', async () => { it('make GET request from endpoint', async () => {
await axios.waitForAll(); await axios.waitForAll();
expect(axiosMock.history.get[0].url).toBe(DEFAULT_PROPS.endpoint); expect(axiosMock.history.get[0].url).toBe(DEFAULT_PROVIDE.endpoint);
}); });
it('generate default option', async () => { it('generate default option', async () => {
...@@ -469,7 +467,7 @@ describe('ForkForm component', () => { ...@@ -469,7 +467,7 @@ describe('ForkForm component', () => {
projectName, projectName,
projectPath, projectPath,
projectVisibility, projectVisibility,
} = DEFAULT_PROPS; } = DEFAULT_PROVIDE;
const url = `/api/${GON_API_VERSION}/projects/${projectId}/fork`; const url = `/api/${GON_API_VERSION}/projects/${projectId}/fork`;
const project = { const project = {
......
import { GlBadge, GlButton, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ForkGroupsListItem from '~/pages/projects/forks/new/components/fork_groups_list_item.vue';
describe('Fork groups list item component', () => {
let wrapper;
const DEFAULT_GROUP_DATA = {
id: 22,
name: 'Gitlab Org',
description: 'Ad et ipsam earum id aut nobis.',
visibility: 'public',
full_name: 'Gitlab Org',
created_at: '2020-06-22T03:32:05.664Z',
updated_at: '2020-06-22T03:32:05.664Z',
avatar_url: null,
fork_path: '/twitter/typeahead-js/-/forks?namespace_key=22',
forked_project_path: null,
permission: 'Owner',
relative_path: '/gitlab-org',
markdown_description:
'<p data-sourcepos="1:1-1:31" dir="auto">Ad et ipsam earum id aut nobis.</p>',
can_create_project: true,
marked_for_deletion: false,
};
const DUMMY_PATH = '/dummy/path';
const createWrapper = (propsData) => {
wrapper = shallowMount(ForkGroupsListItem, {
propsData: {
...propsData,
},
});
};
it('renders pending deletion badge if applicable', () => {
createWrapper({ group: { ...DEFAULT_GROUP_DATA, marked_for_deletion: true } });
expect(wrapper.find(GlBadge).text()).toBe('pending deletion');
});
it('renders go to fork button if has forked project', () => {
createWrapper({ group: { ...DEFAULT_GROUP_DATA, forked_project_path: DUMMY_PATH } });
expect(wrapper.find(GlButton).text()).toBe('Go to fork');
expect(wrapper.find(GlButton).attributes().href).toBe(DUMMY_PATH);
});
it('renders select button if has no forked project', () => {
createWrapper({
group: { ...DEFAULT_GROUP_DATA, forked_project_path: null, fork_path: DUMMY_PATH },
});
expect(wrapper.find(GlButton).text()).toBe('Select');
expect(wrapper.find('form').attributes().action).toBe(DUMMY_PATH);
});
it('renders link to current group', () => {
const DUMMY_FULL_NAME = 'dummy';
createWrapper({
group: { ...DEFAULT_GROUP_DATA, relative_path: DUMMY_PATH, full_name: DUMMY_FULL_NAME },
});
expect(
wrapper
.findAll(GlLink)
.filter((w) => w.text() === DUMMY_FULL_NAME)
.at(0)
.attributes().href,
).toBe(DUMMY_PATH);
});
});
import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import ForkGroupsList from '~/pages/projects/forks/new/components/fork_groups_list.vue';
import ForkGroupsListItem from '~/pages/projects/forks/new/components/fork_groups_list_item.vue';
jest.mock('~/flash');
describe('Fork groups list component', () => {
let wrapper;
let axiosMock;
const DEFAULT_PROPS = {
endpoint: '/dummy',
};
const replyWith = (...args) => axiosMock.onGet(DEFAULT_PROPS.endpoint).reply(...args);
const createWrapper = (propsData) => {
wrapper = shallowMount(ForkGroupsList, {
propsData: {
...DEFAULT_PROPS,
...propsData,
},
stubs: {
GlTabs: {
template: '<div><slot></slot><slot name="tabs-end"></slot></div>',
},
},
});
};
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
});
afterEach(() => {
axiosMock.reset();
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
it('fires load groups request on mount', async () => {
replyWith(200, { namespaces: [] });
createWrapper();
await waitForPromises();
expect(axiosMock.history.get[0].url).toBe(DEFAULT_PROPS.endpoint);
});
it('displays flash if loading groups fails', async () => {
replyWith(500);
createWrapper();
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
});
it('displays loading indicator while loading groups', () => {
replyWith(() => new Promise(() => {}));
createWrapper();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('displays empty text if no groups are available', async () => {
const EMPTY_TEXT = 'No available groups to fork the project.';
replyWith(200, { namespaces: [] });
createWrapper();
await waitForPromises();
expect(wrapper.text()).toContain(EMPTY_TEXT);
});
it('displays filter field when groups are available', async () => {
replyWith(200, { namespaces: [{ name: 'dummy1' }, { name: 'dummy2' }] });
createWrapper();
await waitForPromises();
expect(wrapper.find(GlSearchBoxByType).exists()).toBe(true);
});
it('renders list items for each available group', async () => {
const namespaces = [{ name: 'dummy1' }, { name: 'dummy2' }, { name: 'otherdummy' }];
replyWith(200, { namespaces });
createWrapper();
await waitForPromises();
expect(wrapper.findAll(ForkGroupsListItem)).toHaveLength(namespaces.length);
namespaces.forEach((namespace, idx) => {
expect(wrapper.findAll(ForkGroupsListItem).at(idx).props()).toStrictEqual({
group: namespace,
});
});
});
it('filters repositories on the fly', async () => {
replyWith(200, {
namespaces: [{ name: 'dummy1' }, { name: 'dummy2' }, { name: 'otherdummy' }],
});
createWrapper();
await waitForPromises();
wrapper.find(GlSearchBoxByType).vm.$emit('input', 'other');
await nextTick();
expect(wrapper.findAll(ForkGroupsListItem)).toHaveLength(1);
expect(wrapper.findAll(ForkGroupsListItem).at(0).props().group.name).toBe('otherdummy');
});
});
...@@ -59,26 +59,4 @@ RSpec.describe ForkNamespaceEntity do ...@@ -59,26 +59,4 @@ RSpec.describe ForkNamespaceEntity do
it 'exposes human readable permission level' do it 'exposes human readable permission level' do
expect(json[:permission]).to eql 'Developer' expect(json[:permission]).to eql 'Developer'
end end
it 'exposes can_create_project' do
expect(json[:can_create_project]).to be true
end
context 'when fork_project_form feature flag is disabled' do
before do
stub_feature_flags(fork_project_form: false)
end
it 'sets can_create_project to true when user can create projects in namespace' do
allow(user).to receive(:can?).with(:create_projects, namespace).and_return(true)
expect(json[:can_create_project]).to be true
end
it 'sets can_create_project to false when user is not allowed create projects in namespace' do
allow(user).to receive(:can?).with(:create_projects, namespace).and_return(false)
expect(json[:can_create_project]).to be false
end
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