Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
9a61b300
Commit
9a61b300
authored
Sep 14, 2021
by
Jackie Fraser
Committed by
Savas Vedova
Sep 14, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add import members from project modal [RUN ALL RSPEC]
parent
3813d2a6
Changes
16
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
730 additions
and
18 deletions
+730
-18
app/assets/javascripts/api/projects_api.js
app/assets/javascripts/api/projects_api.js
+8
-0
app/assets/javascripts/invite_members/components/import_a_project_modal.vue
...ipts/invite_members/components/import_a_project_modal.vue
+157
-0
app/assets/javascripts/invite_members/components/project_select.vue
.../javascripts/invite_members/components/project_select.vue
+143
-0
app/assets/javascripts/invite_members/init_import_a_project_modal.js
...javascripts/invite_members/init_import_a_project_modal.js
+23
-0
app/assets/javascripts/pages/projects/project_members/index.js
...ssets/javascripts/pages/projects/project_members/index.js
+2
-0
app/views/projects/project_members/index.html.haml
app/views/projects/project_members/index.html.haml
+1
-4
ee/spec/features/projects/members/invite_group_and_members_spec.rb
...eatures/projects/members/invite_group_and_members_spec.rb
+4
-4
ee/spec/features/projects/settings/user_manages_members_spec.rb
...c/features/projects/settings/user_manages_members_spec.rb
+1
-1
ee/spec/views/projects/project_members/index.html.haml_spec.rb
...ec/views/projects/project_members/index.html.haml_spec.rb
+1
-1
locale/gitlab.pot
locale/gitlab.pot
+33
-3
spec/features/projects/settings/user_manages_project_members_spec.rb
...es/projects/settings/user_manages_project_members_spec.rb
+8
-3
spec/frontend/api/projects_api_spec.js
spec/frontend/api/projects_api_spec.js
+62
-0
spec/frontend/invite_members/components/import_a_project_modal_spec.js
.../invite_members/components/import_a_project_modal_spec.js
+167
-0
spec/frontend/invite_members/components/project_select_spec.js
...frontend/invite_members/components/project_select_spec.js
+105
-0
spec/frontend/invite_members/mock_data/api_response_data.js
spec/frontend/invite_members/mock_data/api_response_data.js
+13
-0
spec/views/projects/project_members/index.html.haml_spec.rb
spec/views/projects/project_members/index.html.haml_spec.rb
+2
-2
No files found.
app/assets/javascripts/api/projects_api.js
View file @
9a61b300
...
@@ -3,6 +3,7 @@ import axios from '../lib/utils/axios_utils';
...
@@ -3,6 +3,7 @@ import axios from '../lib/utils/axios_utils';
import
{
buildApiUrl
}
from
'
./api_utils
'
;
import
{
buildApiUrl
}
from
'
./api_utils
'
;
const
PROJECTS_PATH
=
'
/api/:version/projects.json
'
;
const
PROJECTS_PATH
=
'
/api/:version/projects.json
'
;
const
PROJECT_IMPORT_MEMBERS_PATH
=
'
/api/:version/projects/:id/import_project_members/:project_id
'
;
export
function
getProjects
(
query
,
options
,
callback
=
()
=>
{})
{
export
function
getProjects
(
query
,
options
,
callback
=
()
=>
{})
{
const
url
=
buildApiUrl
(
PROJECTS_PATH
);
const
url
=
buildApiUrl
(
PROJECTS_PATH
);
...
@@ -25,3 +26,10 @@ export function getProjects(query, options, callback = () => {}) {
...
@@ -25,3 +26,10 @@ export function getProjects(query, options, callback = () => {}) {
return
{
data
,
headers
};
return
{
data
,
headers
};
});
});
}
}
export
function
importProjectMembers
(
sourceId
,
targetId
)
{
const
url
=
buildApiUrl
(
PROJECT_IMPORT_MEMBERS_PATH
)
.
replace
(
'
:id
'
,
sourceId
)
.
replace
(
'
:project_id
'
,
targetId
);
return
axios
.
post
(
url
);
}
app/assets/javascripts/invite_members/components/import_a_project_modal.vue
0 → 100644
View file @
9a61b300
<
script
>
import
{
GlButton
,
GlFormGroup
,
GlModal
,
GlModalDirective
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
{
uniqueId
}
from
'
lodash
'
;
import
{
importProjectMembers
}
from
'
~/api/projects_api
'
;
import
{
s__
,
__
,
sprintf
}
from
'
~/locale
'
;
import
ProjectSelect
from
'
./project_select.vue
'
;
export
default
{
components
:
{
GlButton
,
GlFormGroup
,
GlModal
,
GlSprintf
,
ProjectSelect
,
},
directives
:
{
GlModal
:
GlModalDirective
,
},
props
:
{
projectId
:
{
type
:
String
,
required
:
true
,
},
projectName
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
projectToBeImported
:
{},
invalidFeedbackMessage
:
''
,
isLoading
:
false
,
};
},
computed
:
{
modalIntro
()
{
return
sprintf
(
this
.
$options
.
i18n
.
modalIntro
,
{
name
:
this
.
projectName
,
});
},
importDisabled
()
{
return
Object
.
keys
(
this
.
projectToBeImported
).
length
===
0
;
},
validationState
()
{
return
this
.
invalidFeedbackMessage
===
''
?
null
:
false
;
},
},
methods
:
{
submitImport
()
{
this
.
isLoading
=
true
;
return
importProjectMembers
(
this
.
projectId
,
this
.
projectToBeImported
.
id
)
.
then
(
this
.
showToastMessage
)
.
catch
(
this
.
showErrorAlert
)
.
finally
(()
=>
{
this
.
isLoading
=
false
;
this
.
projectToBeImported
=
{};
});
},
closeModal
()
{
this
.
invalidFeedbackMessage
=
''
;
this
.
$refs
.
modal
.
hide
();
},
showToastMessage
()
{
this
.
$toast
.
show
(
this
.
$options
.
i18n
.
successMessage
,
this
.
$options
.
toastOptions
);
this
.
closeModal
();
},
showErrorAlert
()
{
this
.
invalidFeedbackMessage
=
this
.
$options
.
i18n
.
defaultError
;
},
},
toastOptions
()
{
return
{
onComplete
:
()
=>
{
this
.
projectToBeImported
=
{};
},
};
},
i18n
:
{
buttonText
:
s__
(
'
ImportAProjectModal|Import from a project
'
),
projectLabel
:
__
(
'
Project
'
),
modalTitle
:
s__
(
'
ImportAProjectModal|Import members from another project
'
),
modalIntro
:
s__
(
"
ImportAProjectModal|You're importing members to the %{strongStart}%{name}%{strongEnd} project.
"
,
),
modalHelpText
:
s__
(
'
ImportAProjectModal|Only project members (not group members) are imported, and they get the same permissions as the project you import from.
'
,
),
modalPrimaryButton
:
s__
(
'
ImportAProjectModal|Import project members
'
),
modalCancelButton
:
__
(
'
Cancel
'
),
defaultError
:
s__
(
'
ImportAProjectModal|Unable to import project members
'
),
successMessage
:
s__
(
'
ImportAProjectModal|Successfully imported
'
),
},
projectSelectLabelId
:
'
project-select
'
,
modalId
:
uniqueId
(
'
import-a-project-modal-
'
),
formClasses
:
'
gl-mt-3 gl-sm-w-auto gl-w-full
'
,
buttonClasses
:
'
gl-w-full
'
,
};
</
script
>
<
template
>
<form
:class=
"$options.formClasses"
>
<gl-button
v-gl-modal=
"$options.modalId"
:class=
"$options.buttonClasses"
variant=
"default"
>
{{
$options
.
i18n
.
buttonText
}}
</gl-button>
<gl-modal
ref=
"modal"
:modal-id=
"$options.modalId"
size=
"sm"
:title=
"$options.i18n.modalTitle"
ok-variant=
"danger"
footer-class=
"gl-bg-gray-10 gl-p-5"
>
<div>
<p
ref=
"modalIntro"
>
<gl-sprintf
:message=
"modalIntro"
>
<template
#strong
="
{ content }">
<strong>
{{
content
}}
</strong>
</
template
>
</gl-sprintf>
</p>
<gl-form-group
:invalid-feedback=
"invalidFeedbackMessage"
:state=
"validationState"
data-testid=
"form-group"
>
<label
:id=
"$options.projectSelectLabelId"
class=
"col-form-label"
>
{{
$options.i18n.projectLabel
}}
</label>
<project-select
v-model=
"projectToBeImported"
/>
</gl-form-group>
<p>
{{ $options.i18n.modalHelpText }}
</p>
</div>
<
template
#modal-footer
>
<div
class=
"gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0"
>
<gl-button
data-testid=
"cancel-button"
@
click=
"closeModal"
>
{{
$options
.
i18n
.
modalCancelButton
}}
</gl-button>
<div
class=
"gl-mr-3"
></div>
<gl-button
:disabled=
"importDisabled"
:loading=
"isLoading"
variant=
"success"
data-testid=
"import-button"
@
click=
"submitImport"
>
{{
$options
.
i18n
.
modalPrimaryButton
}}
</gl-button
>
</div>
</
template
>
</gl-modal>
</form>
</template>
app/assets/javascripts/invite_members/components/project_select.vue
0 → 100644
View file @
9a61b300
<
script
>
import
{
GlAvatarLabeled
,
GlDropdown
,
GlDropdownItem
,
GlDropdownText
,
GlSearchBoxByType
,
}
from
'
@gitlab/ui
'
;
import
{
debounce
}
from
'
lodash
'
;
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
getProjects
}
from
'
~/rest_api
'
;
import
{
SEARCH_DELAY
,
GROUP_FILTERS
}
from
'
../constants
'
;
export
default
{
name
:
'
ProjectSelect
'
,
components
:
{
GlAvatarLabeled
,
GlDropdown
,
GlDropdownItem
,
GlDropdownText
,
GlSearchBoxByType
,
},
model
:
{
prop
:
'
selectedProject
'
,
},
props
:
{
groupsFilter
:
{
type
:
String
,
required
:
false
,
default
:
GROUP_FILTERS
.
ALL
,
validator
:
(
value
)
=>
Object
.
values
(
GROUP_FILTERS
).
includes
(
value
),
},
parentGroupId
:
{
type
:
Number
,
required
:
false
,
default
:
0
,
},
},
data
()
{
return
{
isFetching
:
false
,
projects
:
[],
selectedProject
:
{},
searchTerm
:
''
,
errorMessage
:
''
,
};
},
computed
:
{
selectedProjectName
()
{
return
this
.
selectedProject
.
name
||
this
.
$options
.
i18n
.
dropdownText
;
},
isFetchResultEmpty
()
{
return
this
.
projects
.
length
===
0
&&
!
this
.
isFetching
;
},
},
watch
:
{
searchTerm
()
{
this
.
retrieveProjects
();
},
},
mounted
()
{
this
.
retrieveProjects
();
},
methods
:
{
retrieveProjects
:
debounce
(
function
debouncedRetrieveProjects
()
{
this
.
isFetching
=
true
;
this
.
errorMessage
=
''
;
return
this
.
fetchProjects
()
.
then
((
response
)
=>
{
this
.
projects
=
response
.
data
.
map
((
project
)
=>
({
...
convertObjectPropsToCamelCase
(
project
),
name
:
project
.
name_with_namespace
,
}));
})
.
catch
(()
=>
{
this
.
errorMessage
=
this
.
$options
.
i18n
.
errorFetchingProjects
;
})
.
finally
(()
=>
{
this
.
isFetching
=
false
;
});
},
SEARCH_DELAY
),
fetchProjects
()
{
return
getProjects
(
this
.
searchTerm
,
this
.
$options
.
defaultFetchOptions
);
},
selectProject
(
project
)
{
this
.
selectedProject
=
project
;
this
.
$emit
(
'
input
'
,
this
.
selectedProject
);
},
},
i18n
:
{
dropdownText
:
s__
(
'
ProjectSelect|Select a project
'
),
searchPlaceholder
:
s__
(
'
ProjectSelect|Search projects
'
),
emptySearchResult
:
s__
(
'
ProjectSelect|No matching results
'
),
errorFetchingProjects
:
s__
(
'
ProjectSelect|There was an error fetching the projects. Please try again.
'
,
),
},
defaultFetchOptions
:
{
exclude_internal
:
true
,
active
:
true
,
},
};
</
script
>
<
template
>
<div>
<gl-dropdown
data-testid=
"project-select-dropdown"
:text=
"selectedProjectName"
toggle-class=
"gl-mb-2"
block
menu-class=
"gl-w-full!"
>
<gl-search-box-by-type
v-model=
"searchTerm"
:is-loading=
"isFetching"
:placeholder=
"$options.i18n.searchPlaceholder"
data-qa-selector=
"project_select_dropdown_search_field"
/>
<gl-dropdown-item
v-for=
"project in projects"
:key=
"project.id"
:name=
"project.name"
@
click=
"selectProject(project)"
>
<gl-avatar-labeled
:label=
"project.name"
:src=
"project.avatarUrl"
:entity-id=
"project.id"
:entity-name=
"project.name"
:size=
"32"
/>
</gl-dropdown-item>
<gl-dropdown-text
v-if=
"errorMessage"
data-testid=
"error-message"
>
<span
class=
"gl-text-gray-500"
>
{{
errorMessage
}}
</span>
</gl-dropdown-text>
<gl-dropdown-text
v-else-if=
"isFetchResultEmpty"
data-testid=
"empty-result-message"
>
<span
class=
"gl-text-gray-500"
>
{{
$options
.
i18n
.
emptySearchResult
}}
</span>
</gl-dropdown-text>
</gl-dropdown>
</div>
</
template
>
app/assets/javascripts/invite_members/init_import_a_project_modal.js
0 → 100644
View file @
9a61b300
import
Vue
from
'
vue
'
;
import
ImportAProjectModal
from
'
~/invite_members/components/import_a_project_modal.vue
'
;
export
default
function
initImportAProjectModal
()
{
const
el
=
document
.
querySelector
(
'
.js-import-a-project-modal
'
);
if
(
!
el
)
{
return
false
;
}
const
{
projectId
,
projectName
}
=
el
.
dataset
;
return
new
Vue
({
el
,
render
:
(
createElement
)
=>
createElement
(
ImportAProjectModal
,
{
props
:
{
projectId
,
projectName
,
},
}),
});
}
app/assets/javascripts/pages/projects/project_members/index.js
View file @
9a61b300
import
groupsSelect
from
'
~/groups_select
'
;
import
groupsSelect
from
'
~/groups_select
'
;
import
initImportAProjectModal
from
'
~/invite_members/init_import_a_project_modal
'
;
import
initInviteGroupTrigger
from
'
~/invite_members/init_invite_group_trigger
'
;
import
initInviteGroupTrigger
from
'
~/invite_members/init_invite_group_trigger
'
;
import
initInviteMembersForm
from
'
~/invite_members/init_invite_members_form
'
;
import
initInviteMembersForm
from
'
~/invite_members/init_invite_members_form
'
;
import
initInviteMembersModal
from
'
~/invite_members/init_invite_members_modal
'
;
import
initInviteMembersModal
from
'
~/invite_members/init_invite_members_modal
'
;
...
@@ -14,6 +15,7 @@ import UsersSelect from '~/users_select';
...
@@ -14,6 +15,7 @@ import UsersSelect from '~/users_select';
groupsSelect
();
groupsSelect
();
memberExpirationDate
();
memberExpirationDate
();
memberExpirationDate
(
'
.js-access-expiration-date-groups
'
);
memberExpirationDate
(
'
.js-access-expiration-date-groups
'
);
initImportAProjectModal
();
initInviteMembersModal
();
initInviteMembersModal
();
initInviteMembersTrigger
();
initInviteMembersTrigger
();
initInviteGroupTrigger
();
initInviteGroupTrigger
();
...
...
app/views/projects/project_members/index.html.haml
View file @
9a61b300
...
@@ -18,10 +18,7 @@
...
@@ -18,10 +18,7 @@
.col-md-12.col-lg-6
.col-md-12.col-lg-6
.gl-display-flex.gl-flex-wrap.gl-justify-content-end
.gl-display-flex.gl-flex-wrap.gl-justify-content-end
-
if
can_admin_project_member?
(
@project
)
-
if
can_admin_project_member?
(
@project
)
=
link_to
_
(
"Import a project"
),
.js-import-a-project-modal
{
data:
{
project_id:
@project
.
id
,
project_name:
@project
.
name
}
}
import_project_project_members_path
(
@project
),
class:
"btn btn-default btn-md gl-button gl-mt-3 gl-sm-w-auto gl-w-full"
,
title:
_
(
"Import members from another project"
)
-
if
@project
.
allowed_to_share_with_group?
-
if
@project
.
allowed_to_share_with_group?
.js-invite-group-trigger
{
data:
{
classes:
'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3'
,
display_text:
_
(
'Invite a group'
)
}
}
.js-invite-group-trigger
{
data:
{
classes:
'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3'
,
display_text:
_
(
'Invite a group'
)
}
}
-
if
can_admin_project_member?
(
@project
)
-
if
can_admin_project_member?
(
@project
)
...
...
ee/spec/features/projects/members/invite_group_and_members_spec.rb
View file @
9a61b300
...
@@ -11,9 +11,9 @@ RSpec.describe 'Project > Members > Invite group and members' do
...
@@ -11,9 +11,9 @@ RSpec.describe 'Project > Members > Invite group and members' do
using
RSpec
::
Parameterized
::
TableSyntax
using
RSpec
::
Parameterized
::
TableSyntax
where
(
:invite_members_group_modal_enabled
,
:expected_invite_member_selector
,
:expected_invite_group_selector
,
:expected_import_button_
text
)
do
where
(
:invite_members_group_modal_enabled
,
:expected_invite_member_selector
,
:expected_invite_group_selector
,
:expected_import_button_
selector
)
do
true
|
'.js-invite-members-trigger'
|
'.js-invite-group-trigger'
|
'
Import a project
'
true
|
'.js-invite-members-trigger'
|
'.js-invite-group-trigger'
|
'
.js-import-a-project-modal
'
false
|
'#invite-member-tab'
|
'#invite-group-tab'
|
'
Impor
t'
false
|
'#invite-member-tab'
|
'#invite-group-tab'
|
'
.invite-users-form .btn-defaul
t'
end
end
with_them
do
with_them
do
...
@@ -31,7 +31,7 @@ RSpec.describe 'Project > Members > Invite group and members' do
...
@@ -31,7 +31,7 @@ RSpec.describe 'Project > Members > Invite group and members' do
expect
(
page
).
to
have_selector
(
expected_invite_member_selector
)
expect
(
page
).
to
have_selector
(
expected_invite_member_selector
)
expect
(
page
).
to
have_selector
(
expected_invite_group_selector
)
expect
(
page
).
to
have_selector
(
expected_invite_group_selector
)
expect
(
page
).
to
have_
link
(
expected_import_button_text
)
expect
(
page
).
to
have_
selector
(
expected_import_button_selector
)
end
end
end
end
...
...
ee/spec/features/projects/settings/user_manages_members_spec.rb
View file @
9a61b300
...
@@ -20,7 +20,7 @@ RSpec.describe "User manages members" do
...
@@ -20,7 +20,7 @@ RSpec.describe "User manages members" do
it
{
expect
(
page
).
to
have_selector
(
".js-invite-members-trigger"
)
}
it
{
expect
(
page
).
to
have_selector
(
".js-invite-members-trigger"
)
}
it
{
expect
(
page
).
to
have_selector
(
".js-invite-group-trigger"
)
}
it
{
expect
(
page
).
to
have_selector
(
".js-invite-group-trigger"
)
}
it
{
expect
(
page
).
to
have_
link
(
"Import a project
"
)
}
it
{
expect
(
page
).
to
have_
selector
(
".js-import-a-project-modal
"
)
}
end
end
shared_examples
"when group membership is locked"
do
shared_examples
"when group membership is locked"
do
...
...
ee/spec/views/projects/project_members/index.html.haml_spec.rb
View file @
9a61b300
...
@@ -27,7 +27,7 @@ RSpec.describe 'projects/project_members/index', :aggregate_failures do
...
@@ -27,7 +27,7 @@ RSpec.describe 'projects/project_members/index', :aggregate_failures do
expect
(
rendered
).
to
have_content
(
'Project members'
)
expect
(
rendered
).
to
have_content
(
'Project members'
)
expect
(
rendered
).
to
have_content
(
'You can invite another group to'
)
expect
(
rendered
).
to
have_content
(
'You can invite another group to'
)
expect
(
rendered
).
not_to
have_link
(
'Import a project'
)
expect
(
rendered
).
not_to
have_link
(
'Import
from
a project'
)
expect
(
rendered
).
to
have_selector
(
'.js-invite-group-trigger'
)
expect
(
rendered
).
to
have_selector
(
'.js-invite-group-trigger'
)
expect
(
rendered
).
not_to
have_selector
(
'.js-invite-members-trigger'
)
expect
(
rendered
).
not_to
have_selector
(
'.js-invite-members-trigger'
)
expect
(
rendered
).
not_to
have_content
(
'Members can be added by project'
)
expect
(
rendered
).
not_to
have_content
(
'Members can be added by project'
)
...
...
locale/gitlab.pot
View file @
9a61b300
...
@@ -17007,9 +17007,6 @@ msgstr ""
...
@@ -17007,9 +17007,6 @@ msgstr ""
msgid "Import Projects from Gitea"
msgid "Import Projects from Gitea"
msgstr ""
msgstr ""
msgid "Import a project"
msgstr ""
msgid "Import an exported GitLab project"
msgid "Import an exported GitLab project"
msgstr ""
msgstr ""
...
@@ -17094,6 +17091,27 @@ msgstr ""
...
@@ -17094,6 +17091,27 @@ msgstr ""
msgid "Import timed out. Import took longer than %{import_jobs_expiration} seconds"
msgid "Import timed out. Import took longer than %{import_jobs_expiration} seconds"
msgstr ""
msgstr ""
msgid "ImportAProjectModal|Import from a project"
msgstr ""
msgid "ImportAProjectModal|Import members from another project"
msgstr ""
msgid "ImportAProjectModal|Import project members"
msgstr ""
msgid "ImportAProjectModal|Only project members (not group members) are imported, and they get the same permissions as the project you import from."
msgstr ""
msgid "ImportAProjectModal|Successfully imported"
msgstr ""
msgid "ImportAProjectModal|Unable to import project members"
msgstr ""
msgid "ImportAProjectModal|You're importing members to the %{strongStart}%{name}%{strongEnd} project."
msgstr ""
msgid "ImportButtons|Connect repositories from"
msgid "ImportButtons|Connect repositories from"
msgstr ""
msgstr ""
...
@@ -26191,9 +26209,21 @@ msgstr ""
...
@@ -26191,9 +26209,21 @@ msgstr ""
msgid "ProjectSelect| or group"
msgid "ProjectSelect| or group"
msgstr ""
msgstr ""
msgid "ProjectSelect|No matching results"
msgstr ""
msgid "ProjectSelect|Search for project"
msgid "ProjectSelect|Search for project"
msgstr ""
msgstr ""
msgid "ProjectSelect|Search projects"
msgstr ""
msgid "ProjectSelect|Select a project"
msgstr ""
msgid "ProjectSelect|There was an error fetching the projects. Please try again."
msgstr ""
msgid "ProjectService|Drone server URL"
msgid "ProjectService|Drone server URL"
msgstr ""
msgstr ""
...
...
spec/features/projects/settings/user_manages_project_members_spec.rb
View file @
9a61b300
...
@@ -43,10 +43,15 @@ RSpec.describe 'Projects > Settings > User manages project members' do
...
@@ -43,10 +43,15 @@ RSpec.describe 'Projects > Settings > User manages project members' do
visit
(
project_project_members_path
(
project
))
visit
(
project_project_members_path
(
project
))
click_link
(
'Import a project'
)
click_on
'Import from a project'
click_on
'Select a project'
wait_for_requests
select2
(
project2
.
id
,
from:
'#source_project_id'
)
click_button
project2
.
name
click_button
(
'Import project members'
)
click_button
'Import project members'
wait_for_requests
page
.
refresh
expect
(
find_member_row
(
user_mike
)).
to
have_content
(
'Reporter'
)
expect
(
find_member_row
(
user_mike
)).
to
have_content
(
'Reporter'
)
end
end
...
...
spec/frontend/api/projects_api_spec.js
0 → 100644
View file @
9a61b300
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
*
as
projectsApi
from
'
~/api/projects_api
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
describe
(
'
~/api/projects_api.js
'
,
()
=>
{
let
mock
;
let
originalGon
;
const
projectId
=
1
;
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
originalGon
=
window
.
gon
;
window
.
gon
=
{
api_version
:
'
v7
'
};
});
afterEach
(()
=>
{
mock
.
restore
();
window
.
gon
=
originalGon
;
});
describe
(
'
getProjects
'
,
()
=>
{
beforeEach
(()
=>
{
jest
.
spyOn
(
axios
,
'
get
'
);
});
it
(
'
retrieves projects from the correct URL and returns them in the response data
'
,
()
=>
{
const
expectedUrl
=
'
/api/v7/projects.json
'
;
const
expectedParams
=
{
params
:
{
per_page
:
20
,
search
:
''
,
simple
:
true
}
};
const
expectedProjects
=
[{
name
:
'
project 1
'
}];
const
query
=
''
;
const
options
=
{};
mock
.
onGet
(
expectedUrl
).
reply
(
200
,
{
data
:
expectedProjects
});
return
projectsApi
.
getProjects
(
query
,
options
).
then
(({
data
})
=>
{
expect
(
axios
.
get
).
toHaveBeenCalledWith
(
expectedUrl
,
expectedParams
);
expect
(
data
.
data
).
toEqual
(
expectedProjects
);
});
});
});
describe
(
'
importProjectMembers
'
,
()
=>
{
beforeEach
(()
=>
{
jest
.
spyOn
(
axios
,
'
post
'
);
});
it
(
'
posts to the correct URL and returns the response message
'
,
()
=>
{
const
targetId
=
2
;
const
expectedUrl
=
'
/api/v7/projects/1/import_project_members/2
'
;
const
expectedMessage
=
'
Successfully imported
'
;
mock
.
onPost
(
expectedUrl
).
replyOnce
(
200
,
expectedMessage
);
return
projectsApi
.
importProjectMembers
(
projectId
,
targetId
).
then
(({
data
})
=>
{
expect
(
axios
.
post
).
toHaveBeenCalledWith
(
expectedUrl
);
expect
(
data
).
toEqual
(
expectedMessage
);
});
});
});
});
spec/frontend/invite_members/components/import_a_project_modal_spec.js
0 → 100644
View file @
9a61b300
import
{
GlFormGroup
,
GlSprintf
,
GlModal
}
from
'
@gitlab/ui
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
{
stubComponent
}
from
'
helpers/stub_component
'
;
import
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
*
as
ProjectsApi
from
'
~/api/projects_api
'
;
import
ImportAProjectModal
from
'
~/invite_members/components/import_a_project_modal.vue
'
;
import
ProjectSelect
from
'
~/invite_members/components/project_select.vue
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
let
wrapper
;
let
mock
;
const
projectId
=
'
1
'
;
const
projectName
=
'
test name
'
;
const
projectToBeImported
=
{
id
:
'
2
'
};
const
$toast
=
{
show
:
jest
.
fn
(),
};
const
createComponent
=
()
=>
{
wrapper
=
shallowMountExtended
(
ImportAProjectModal
,
{
propsData
:
{
projectId
,
projectName
,
},
stubs
:
{
GlModal
:
stubComponent
(
GlModal
,
{
template
:
'
<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>
'
,
}),
GlSprintf
,
GlFormGroup
:
stubComponent
(
GlFormGroup
,
{
props
:
[
'
state
'
,
'
invalidFeedback
'
],
}),
},
mocks
:
{
$toast
,
},
});
};
beforeEach
(()
=>
{
gon
.
api_version
=
'
v4
'
;
mock
=
new
MockAdapter
(
axios
);
});
afterEach
(()
=>
{
wrapper
.
destroy
();
mock
.
restore
();
});
describe
(
'
ImportAProjectModal
'
,
()
=>
{
const
findIntroText
=
()
=>
wrapper
.
find
({
ref
:
'
modalIntro
'
}).
text
();
const
findCancelButton
=
()
=>
wrapper
.
findByTestId
(
'
cancel-button
'
);
const
findImportButton
=
()
=>
wrapper
.
findByTestId
(
'
import-button
'
);
const
clickImportButton
=
()
=>
findImportButton
().
vm
.
$emit
(
'
click
'
);
const
clickCancelButton
=
()
=>
findCancelButton
().
vm
.
$emit
(
'
click
'
);
const
findFormGroup
=
()
=>
wrapper
.
findByTestId
(
'
form-group
'
);
const
formGroupInvalidFeedback
=
()
=>
findFormGroup
().
props
(
'
invalidFeedback
'
);
const
formGroupErrorState
=
()
=>
findFormGroup
().
props
(
'
state
'
);
const
findProjectSelect
=
()
=>
wrapper
.
findComponent
(
ProjectSelect
);
describe
(
'
rendering the modal
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
(
'
renders the modal with the correct title
'
,
()
=>
{
expect
(
wrapper
.
findComponent
(
GlModal
).
props
(
'
title
'
)).
toBe
(
'
Import members from another project
'
,
);
});
it
(
'
renders the Cancel button text correctly
'
,
()
=>
{
expect
(
findCancelButton
().
text
()).
toBe
(
'
Cancel
'
);
});
it
(
'
renders the Import button text correctly
'
,
()
=>
{
expect
(
findImportButton
().
text
()).
toBe
(
'
Import project members
'
);
});
it
(
'
renders the modal intro text correctly
'
,
()
=>
{
expect
(
findIntroText
()).
toBe
(
"
You're importing members to the test name project.
"
);
});
it
(
'
renders the Import button modal without isLoading
'
,
()
=>
{
expect
(
findImportButton
().
props
(
'
loading
'
)).
toBe
(
false
);
});
it
(
'
sets isLoading to true when the Invite button is clicked
'
,
async
()
=>
{
clickImportButton
();
await
wrapper
.
vm
.
$nextTick
();
expect
(
findImportButton
().
props
(
'
loading
'
)).
toBe
(
true
);
});
});
describe
(
'
submitting the import form
'
,
()
=>
{
describe
(
'
when the import is successful
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
findProjectSelect
().
vm
.
$emit
(
'
input
'
,
projectToBeImported
);
jest
.
spyOn
(
ProjectsApi
,
'
importProjectMembers
'
).
mockResolvedValue
();
clickImportButton
();
});
it
(
'
calls Api importProjectMembers
'
,
()
=>
{
expect
(
ProjectsApi
.
importProjectMembers
).
toHaveBeenCalledWith
(
projectId
,
projectToBeImported
.
id
,
);
});
it
(
'
displays the successful toastMessage
'
,
()
=>
{
expect
(
$toast
.
show
).
toHaveBeenCalledWith
(
'
Successfully imported
'
,
wrapper
.
vm
.
$options
.
toastOptions
,
);
});
it
(
'
sets isLoading to false after success
'
,
()
=>
{
expect
(
findImportButton
().
props
(
'
loading
'
)).
toBe
(
false
);
});
});
describe
(
'
when the import fails
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createComponent
();
findProjectSelect
().
vm
.
$emit
(
'
input
'
,
projectToBeImported
);
jest
.
spyOn
(
ProjectsApi
,
'
importProjectMembers
'
)
.
mockRejectedValue
({
response
:
{
data
:
{
success
:
false
}
}
});
clickImportButton
();
await
waitForPromises
();
});
it
(
'
displays the generic error message
'
,
()
=>
{
expect
(
formGroupInvalidFeedback
()).
toBe
(
'
Unable to import project members
'
);
expect
(
formGroupErrorState
()).
toBe
(
false
);
});
it
(
'
sets isLoading to false after error
'
,
()
=>
{
expect
(
findImportButton
().
props
(
'
loading
'
)).
toBe
(
false
);
});
it
(
'
clears the error when the modal is closed with an error
'
,
async
()
=>
{
expect
(
formGroupInvalidFeedback
()).
toBe
(
'
Unable to import project members
'
);
expect
(
formGroupErrorState
()).
toBe
(
false
);
clickCancelButton
();
await
wrapper
.
vm
.
$nextTick
();
expect
(
formGroupInvalidFeedback
()).
toBe
(
''
);
expect
(
formGroupErrorState
()).
not
.
toBe
(
false
);
});
});
});
});
spec/frontend/invite_members/components/project_select_spec.js
0 → 100644
View file @
9a61b300
import
{
GlSearchBoxByType
,
GlAvatarLabeled
,
GlDropdownItem
}
from
'
@gitlab/ui
'
;
import
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
*
as
projectsApi
from
'
~/api/projects_api
'
;
import
ProjectSelect
from
'
~/invite_members/components/project_select.vue
'
;
import
{
allProjects
,
project1
}
from
'
../mock_data/api_response_data
'
;
describe
(
'
ProjectSelect
'
,
()
=>
{
let
wrapper
;
const
createComponent
=
()
=>
{
wrapper
=
shallowMountExtended
(
ProjectSelect
,
{});
};
beforeEach
(()
=>
{
jest
.
spyOn
(
projectsApi
,
'
getProjects
'
).
mockResolvedValue
(
allProjects
);
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
const
findSearchBoxByType
=
()
=>
wrapper
.
findComponent
(
GlSearchBoxByType
);
const
findDropdownItem
=
(
index
)
=>
wrapper
.
findAllComponents
(
GlDropdownItem
).
at
(
index
);
const
findAvatarLabeled
=
(
index
)
=>
findDropdownItem
(
index
).
findComponent
(
GlAvatarLabeled
);
const
findEmptyResultMessage
=
()
=>
wrapper
.
findByTestId
(
'
empty-result-message
'
);
const
findErrorMessage
=
()
=>
wrapper
.
findByTestId
(
'
error-message
'
);
it
(
'
renders GlSearchBoxByType with default attributes
'
,
()
=>
{
expect
(
findSearchBoxByType
().
exists
()).
toBe
(
true
);
expect
(
findSearchBoxByType
().
vm
.
$attrs
).
toMatchObject
({
placeholder
:
'
Search projects
'
,
});
});
describe
(
'
when user types in the search input
'
,
()
=>
{
let
resolveApiRequest
;
let
rejectApiRequest
;
beforeEach
(()
=>
{
jest
.
spyOn
(
projectsApi
,
'
getProjects
'
).
mockImplementation
(
()
=>
new
Promise
((
resolve
,
reject
)
=>
{
resolveApiRequest
=
resolve
;
rejectApiRequest
=
reject
;
}),
);
findSearchBoxByType
().
vm
.
$emit
(
'
input
'
,
project1
.
name
);
});
it
(
'
calls the API
'
,
()
=>
{
resolveApiRequest
({
data
:
allProjects
});
expect
(
projectsApi
.
getProjects
).
toHaveBeenCalledWith
(
project1
.
name
,
{
active
:
true
,
exclude_internal
:
true
,
});
});
it
(
'
displays loading icon while waiting for API call to resolve and then sets loading false
'
,
async
()
=>
{
expect
(
findSearchBoxByType
().
props
(
'
isLoading
'
)).
toBe
(
true
);
resolveApiRequest
({
data
:
allProjects
});
await
waitForPromises
();
expect
(
findSearchBoxByType
().
props
(
'
isLoading
'
)).
toBe
(
false
);
expect
(
findEmptyResultMessage
().
exists
()).
toBe
(
false
);
expect
(
findErrorMessage
().
exists
()).
toBe
(
false
);
});
it
(
'
displays a dropdown item and avatar for each project fetched
'
,
async
()
=>
{
resolveApiRequest
({
data
:
allProjects
});
await
waitForPromises
();
allProjects
.
forEach
((
project
,
index
)
=>
{
expect
(
findDropdownItem
(
index
).
attributes
(
'
name
'
)).
toBe
(
project
.
name_with_namespace
);
expect
(
findAvatarLabeled
(
index
).
attributes
()).
toMatchObject
({
src
:
project
.
avatar_url
,
'
entity-id
'
:
String
(
project
.
id
),
'
entity-name
'
:
project
.
name_with_namespace
,
});
expect
(
findAvatarLabeled
(
index
).
props
(
'
label
'
)).
toBe
(
project
.
name_with_namespace
);
});
});
it
(
'
displays the empty message when the API results are empty
'
,
async
()
=>
{
resolveApiRequest
({
data
:
[]
});
await
waitForPromises
();
expect
(
findEmptyResultMessage
().
text
()).
toBe
(
'
No matching results
'
);
});
it
(
'
displays the error message when the fetch fails
'
,
async
()
=>
{
rejectApiRequest
();
await
waitForPromises
();
expect
(
findErrorMessage
().
text
()).
toBe
(
'
There was an error fetching the projects. Please try again.
'
,
);
});
});
});
spec/frontend/invite_members/mock_data/api_response_data.js
0 → 100644
View file @
9a61b300
export
const
project1
=
{
id
:
1
,
name
:
'
Project One
'
,
name_with_namespace
:
'
Project One
'
,
avatar_url
:
'
test1
'
,
};
export
const
project2
=
{
id
:
2
,
name
:
'
Project One
'
,
name_with_namespace
:
'
Project Two
'
,
avatar_url
:
'
test2
'
,
};
export
const
allProjects
=
[
project1
,
project2
];
spec/views/projects/project_members/index.html.haml_spec.rb
View file @
9a61b300
...
@@ -25,7 +25,7 @@ RSpec.describe 'projects/project_members/index', :aggregate_failures do
...
@@ -25,7 +25,7 @@ RSpec.describe 'projects/project_members/index', :aggregate_failures do
expect
(
rendered
).
to
have_content
(
'Project members'
)
expect
(
rendered
).
to
have_content
(
'Project members'
)
expect
(
rendered
).
to
have_content
(
'You can invite a new member'
)
expect
(
rendered
).
to
have_content
(
'You can invite a new member'
)
expect
(
rendered
).
to
have_
link
(
'Import a project
'
)
expect
(
rendered
).
to
have_
selector
(
'.js-import-a-project-modal
'
)
expect
(
rendered
).
to
have_selector
(
'.js-invite-group-trigger'
)
expect
(
rendered
).
to
have_selector
(
'.js-invite-group-trigger'
)
expect
(
rendered
).
to
have_selector
(
'.js-invite-members-trigger'
)
expect
(
rendered
).
to
have_selector
(
'.js-invite-members-trigger'
)
expect
(
rendered
).
not_to
have_content
(
'Members can be added by project'
)
expect
(
rendered
).
not_to
have_content
(
'Members can be added by project'
)
...
@@ -85,7 +85,7 @@ RSpec.describe 'projects/project_members/index', :aggregate_failures do
...
@@ -85,7 +85,7 @@ RSpec.describe 'projects/project_members/index', :aggregate_failures do
expect
(
rendered
).
to
have_content
(
'Project members'
)
expect
(
rendered
).
to
have_content
(
'Project members'
)
expect
(
rendered
).
not_to
have_content
(
'You can invite a new member'
)
expect
(
rendered
).
not_to
have_content
(
'You can invite a new member'
)
expect
(
rendered
).
not_to
have_
link
(
'Import a project
'
)
expect
(
rendered
).
not_to
have_
selector
(
'.js-import-a-project-modal
'
)
expect
(
rendered
).
not_to
have_selector
(
'.js-invite-group-trigger'
)
expect
(
rendered
).
not_to
have_selector
(
'.js-invite-group-trigger'
)
expect
(
rendered
).
not_to
have_selector
(
'.js-invite-members-trigger'
)
expect
(
rendered
).
not_to
have_selector
(
'.js-invite-members-trigger'
)
expect
(
rendered
).
to
have_content
(
'Members can be added by project'
)
expect
(
rendered
).
to
have_content
(
'Members can be added by project'
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment