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
fa18ec94
Commit
fa18ec94
authored
Aug 02, 2021
by
Doug Stull
Committed by
Brandon Labuschagne
Aug 02, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Experiment: add area of focus question to invite modal
parent
5c794438
Changes
12
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
348 additions
and
51 deletions
+348
-51
app/assets/javascripts/invite_members/components/invite_members_modal.vue
...cripts/invite_members/components/invite_members_modal.vue
+52
-9
app/assets/javascripts/invite_members/constants.js
app/assets/javascripts/invite_members/constants.js
+5
-0
app/assets/javascripts/invite_members/init_invite_members_modal.js
...s/javascripts/invite_members/init_invite_members_modal.js
+2
-0
app/helpers/invite_members_helper.rb
app/helpers/invite_members_helper.rb
+39
-0
app/views/groups/_invite_members_modal.html.haml
app/views/groups/_invite_members_modal.html.haml
+2
-5
app/views/projects/_invite_members_modal.html.haml
app/views/projects/_invite_members_modal.html.haml
+2
-5
config/feature_flags/experiment/member_areas_of_focus.yml
config/feature_flags/experiment/member_areas_of_focus.yml
+8
-0
locale/gitlab.pot
locale/gitlab.pot
+18
-0
spec/features/groups/members/manage_members_spec.rb
spec/features/groups/members/manage_members_spec.rb
+54
-0
spec/frontend/invite_members/components/invite_members_modal_spec.js
...nd/invite_members/components/invite_members_modal_spec.js
+106
-30
spec/helpers/invite_members_helper_spec.rb
spec/helpers/invite_members_helper_spec.rb
+50
-0
spec/support/helpers/features/invite_members_modal_helper.rb
spec/support/helpers/features/invite_members_modal_helper.rb
+10
-2
No files found.
app/assets/javascripts/invite_members/components/invite_members_modal.vue
View file @
fa18ec94
...
@@ -9,13 +9,14 @@ import {
...
@@ -9,13 +9,14 @@ import {
GlSprintf
,
GlSprintf
,
GlButton
,
GlButton
,
GlFormInput
,
GlFormInput
,
GlFormCheckboxGroup
,
}
from
'
@gitlab/ui
'
;
}
from
'
@gitlab/ui
'
;
import
{
partition
,
isString
}
from
'
lodash
'
;
import
{
partition
,
isString
}
from
'
lodash
'
;
import
Api
from
'
~/api
'
;
import
Api
from
'
~/api
'
;
import
ExperimentTracking
from
'
~/experimentation/experiment_tracking
'
;
import
ExperimentTracking
from
'
~/experimentation/experiment_tracking
'
;
import
{
BV_SHOW_MODAL
,
BV_HIDE_MODAL
}
from
'
~/lib/utils/constants
'
;
import
{
BV_SHOW_MODAL
}
from
'
~/lib/utils/constants
'
;
import
{
s__
,
sprintf
}
from
'
~/locale
'
;
import
{
s__
,
sprintf
}
from
'
~/locale
'
;
import
{
INVITE_MEMBERS_IN_COMMENT
,
GROUP_FILTERS
}
from
'
../constants
'
;
import
{
INVITE_MEMBERS_IN_COMMENT
,
GROUP_FILTERS
,
MEMBER_AREAS_OF_FOCUS
}
from
'
../constants
'
;
import
eventHub
from
'
../event_hub
'
;
import
eventHub
from
'
../event_hub
'
;
import
{
import
{
responseMessageFromError
,
responseMessageFromError
,
...
@@ -36,6 +37,7 @@ export default {
...
@@ -36,6 +37,7 @@ export default {
GlSprintf
,
GlSprintf
,
GlButton
,
GlButton
,
GlFormInput
,
GlFormInput
,
GlFormCheckboxGroup
,
MembersTokenSelect
,
MembersTokenSelect
,
GroupSelect
,
GroupSelect
,
},
},
...
@@ -74,6 +76,14 @@ export default {
...
@@ -74,6 +76,14 @@ export default {
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
},
},
areasOfFocusOptions
:
{
type
:
Array
,
required
:
true
,
},
noSelectionAreasOfFocus
:
{
type
:
Array
,
required
:
true
,
},
},
},
data
()
{
data
()
{
return
{
return
{
...
@@ -83,6 +93,7 @@ export default {
...
@@ -83,6 +93,7 @@ export default {
inviteeType
:
'
members
'
,
inviteeType
:
'
members
'
,
newUsersToInvite
:
[],
newUsersToInvite
:
[],
selectedDate
:
undefined
,
selectedDate
:
undefined
,
selectedAreasOfFocus
:
[],
groupToBeSharedWith
:
{},
groupToBeSharedWith
:
{},
source
:
'
unknown
'
,
source
:
'
unknown
'
,
invalidFeedbackMessage
:
''
,
invalidFeedbackMessage
:
''
,
...
@@ -128,10 +139,21 @@ export default {
...
@@ -128,10 +139,21 @@ export default {
this
.
newUsersToInvite
.
length
===
0
&&
Object
.
keys
(
this
.
groupToBeSharedWith
).
length
===
0
this
.
newUsersToInvite
.
length
===
0
&&
Object
.
keys
(
this
.
groupToBeSharedWith
).
length
===
0
);
);
},
},
areasOfFocusEnabled
()
{
return
this
.
areasOfFocusOptions
.
length
!==
0
;
},
areasOfFocusForPost
()
{
if
(
this
.
selectedAreasOfFocus
.
length
===
0
&&
this
.
areasOfFocusEnabled
)
{
return
this
.
noSelectionAreasOfFocus
;
}
return
this
.
selectedAreasOfFocus
;
},
},
},
mounted
()
{
mounted
()
{
eventHub
.
$on
(
'
openModal
'
,
(
options
)
=>
{
eventHub
.
$on
(
'
openModal
'
,
(
options
)
=>
{
this
.
openModal
(
options
);
this
.
openModal
(
options
);
this
.
trackEvent
(
MEMBER_AREAS_OF_FOCUS
.
name
,
MEMBER_AREAS_OF_FOCUS
.
view
);
});
});
},
},
methods
:
{
methods
:
{
...
@@ -152,9 +174,12 @@ export default {
...
@@ -152,9 +174,12 @@ export default {
this
.
$root
.
$emit
(
BV_SHOW_MODAL
,
this
.
modalId
);
this
.
$root
.
$emit
(
BV_SHOW_MODAL
,
this
.
modalId
);
},
},
trackEvent
(
experimentName
,
eventName
)
{
const
tracking
=
new
ExperimentTracking
(
experimentName
);
tracking
.
event
(
eventName
);
},
closeModal
()
{
closeModal
()
{
this
.
resetFields
();
this
.
$refs
.
modal
.
hide
();
this
.
$root
.
$emit
(
BV_HIDE_MODAL
,
this
.
modalId
);
},
},
sendInvite
()
{
sendInvite
()
{
if
(
this
.
isInviteGroup
)
{
if
(
this
.
isInviteGroup
)
{
...
@@ -165,9 +190,10 @@ export default {
...
@@ -165,9 +190,10 @@ export default {
},
},
trackInvite
()
{
trackInvite
()
{
if
(
this
.
source
===
INVITE_MEMBERS_IN_COMMENT
)
{
if
(
this
.
source
===
INVITE_MEMBERS_IN_COMMENT
)
{
const
tracking
=
new
ExperimentTracking
(
INVITE_MEMBERS_IN_COMMENT
);
this
.
trackEvent
(
INVITE_MEMBERS_IN_COMMENT
,
'
comment_invite_success
'
);
tracking
.
event
(
'
comment_invite_success
'
);
}
}
this
.
trackEvent
(
MEMBER_AREAS_OF_FOCUS
.
name
,
MEMBER_AREAS_OF_FOCUS
.
submit
);
},
},
resetFields
()
{
resetFields
()
{
this
.
isLoading
=
false
;
this
.
isLoading
=
false
;
...
@@ -176,6 +202,7 @@ export default {
...
@@ -176,6 +202,7 @@ export default {
this
.
newUsersToInvite
=
[];
this
.
newUsersToInvite
=
[];
this
.
groupToBeSharedWith
=
{};
this
.
groupToBeSharedWith
=
{};
this
.
invalidFeedbackMessage
=
''
;
this
.
invalidFeedbackMessage
=
''
;
this
.
selectedAreasOfFocus
=
[];
},
},
changeSelectedItem
(
item
)
{
changeSelectedItem
(
item
)
{
this
.
selectedAccessLevel
=
item
;
this
.
selectedAccessLevel
=
item
;
...
@@ -223,6 +250,7 @@ export default {
...
@@ -223,6 +250,7 @@ export default {
email
:
usersToInviteByEmail
,
email
:
usersToInviteByEmail
,
access_level
:
this
.
selectedAccessLevel
,
access_level
:
this
.
selectedAccessLevel
,
invite_source
:
this
.
source
,
invite_source
:
this
.
source
,
areas_of_focus
:
this
.
areasOfFocusForPost
,
};
};
},
},
addByUserIdPostData
(
usersToAddById
)
{
addByUserIdPostData
(
usersToAddById
)
{
...
@@ -231,6 +259,7 @@ export default {
...
@@ -231,6 +259,7 @@ export default {
user_id
:
usersToAddById
,
user_id
:
usersToAddById
,
access_level
:
this
.
selectedAccessLevel
,
access_level
:
this
.
selectedAccessLevel
,
invite_source
:
this
.
source
,
invite_source
:
this
.
source
,
areas_of_focus
:
this
.
areasOfFocusForPost
,
};
};
},
},
shareWithGroupPostData
(
groupToBeSharedWith
)
{
shareWithGroupPostData
(
groupToBeSharedWith
)
{
...
@@ -304,18 +333,22 @@ export default {
...
@@ -304,18 +333,22 @@ export default {
inviteButtonText
:
s__
(
'
InviteMembersModal|Invite
'
),
inviteButtonText
:
s__
(
'
InviteMembersModal|Invite
'
),
cancelButtonText
:
s__
(
'
InviteMembersModal|Cancel
'
),
cancelButtonText
:
s__
(
'
InviteMembersModal|Cancel
'
),
headerCloseLabel
:
s__
(
'
InviteMembersModal|Close invite team members
'
),
headerCloseLabel
:
s__
(
'
InviteMembersModal|Close invite team members
'
),
areasOfFocusLabel
:
s__
(
'
InviteMembersModal|What would you like new member(s) to focus on? (optional)
'
,
),
},
},
membersTokenSelectLabelId
:
'
invite-members-input
'
,
membersTokenSelectLabelId
:
'
invite-members-input
'
,
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<gl-modal
<gl-modal
ref=
"modal"
:modal-id=
"modalId"
:modal-id=
"modalId"
size=
"sm"
size=
"sm"
data-qa-selector=
"invite_members_modal_content"
data-qa-selector=
"invite_members_modal_content"
:title=
"$options.labels[inviteeType].modalTitle"
:title=
"$options.labels[inviteeType].modalTitle"
:header-close-label=
"$options.labels.headerCloseLabel"
:header-close-label=
"$options.labels.headerCloseLabel"
@
close
=
"resetFields"
@
hidden
=
"resetFields"
>
>
<div>
<div>
<p
ref=
"introText"
>
<p
ref=
"introText"
>
...
@@ -351,7 +384,7 @@ export default {
...
@@ -351,7 +384,7 @@ export default {
/>
/>
</gl-form-group>
</gl-form-group>
<label
class=
"gl-
font-weight-bold gl-
mt-3"
>
{{ $options.labels.accessLevel }}
</label>
<label
class=
"gl-mt-3"
>
{{ $options.labels.accessLevel }}
</label>
<div
class=
"gl-mt-2 gl-w-half gl-xs-w-full"
>
<div
class=
"gl-mt-2 gl-w-half gl-xs-w-full"
>
<gl-dropdown
<gl-dropdown
class=
"gl-shadow-none gl-w-full"
class=
"gl-shadow-none gl-w-full"
...
@@ -381,7 +414,7 @@ export default {
...
@@ -381,7 +414,7 @@ export default {
</gl-sprintf>
</gl-sprintf>
</div>
</div>
<label
class=
"gl-
font-weight-bold gl-
mt-5 gl-display-block"
for=
"expires_at"
>
{{
<label
class=
"gl-mt-5 gl-display-block"
for=
"expires_at"
>
{{
$options.labels.accessExpireDate
$options.labels.accessExpireDate
}}
</label>
}}
</label>
<div
class=
"gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block"
>
<div
class=
"gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block"
>
...
@@ -400,6 +433,16 @@ export default {
...
@@ -400,6 +433,16 @@ export default {
</
template
>
</
template
>
</gl-datepicker>
</gl-datepicker>
</div>
</div>
<div
v-if=
"areasOfFocusEnabled"
>
<label
class=
"gl-mt-5"
>
{{ $options.labels.areasOfFocusLabel }}
</label>
<gl-form-checkbox-group
v-model=
"selectedAreasOfFocus"
:options=
"areasOfFocusOptions"
data-testid=
"area-of-focus-checks"
/>
</div>
</div>
</div>
<
template
#modal-footer
>
<
template
#modal-footer
>
...
...
app/assets/javascripts/invite_members/constants.js
View file @
fa18ec94
...
@@ -3,6 +3,11 @@ import { __ } from '~/locale';
...
@@ -3,6 +3,11 @@ import { __ } from '~/locale';
export
const
SEARCH_DELAY
=
200
;
export
const
SEARCH_DELAY
=
200
;
export
const
INVITE_MEMBERS_IN_COMMENT
=
'
invite_members_in_comment
'
;
export
const
INVITE_MEMBERS_IN_COMMENT
=
'
invite_members_in_comment
'
;
export
const
MEMBER_AREAS_OF_FOCUS
=
{
name
:
'
member_areas_of_focus
'
,
view
:
'
view
'
,
submit
:
'
submit
'
,
};
export
const
GROUP_FILTERS
=
{
export
const
GROUP_FILTERS
=
{
ALL
:
'
all
'
,
ALL
:
'
all
'
,
...
...
app/assets/javascripts/invite_members/init_invite_members_modal.js
View file @
fa18ec94
...
@@ -23,6 +23,8 @@ export default function initInviteMembersModal() {
...
@@ -23,6 +23,8 @@ export default function initInviteMembersModal() {
defaultAccessLevel
:
parseInt
(
el
.
dataset
.
defaultAccessLevel
,
10
),
defaultAccessLevel
:
parseInt
(
el
.
dataset
.
defaultAccessLevel
,
10
),
groupSelectFilter
:
el
.
dataset
.
groupsFilter
,
groupSelectFilter
:
el
.
dataset
.
groupsFilter
,
groupSelectParentId
:
parseInt
(
el
.
dataset
.
parentId
,
10
),
groupSelectParentId
:
parseInt
(
el
.
dataset
.
parentId
,
10
),
areasOfFocusOptions
:
JSON
.
parse
(
el
.
dataset
.
areasOfFocusOptions
),
noSelectionAreasOfFocus
:
JSON
.
parse
(
el
.
dataset
.
noSelectionAreasOfFocus
),
},
},
}),
}),
});
});
...
...
app/helpers/invite_members_helper.rb
View file @
fa18ec94
...
@@ -39,4 +39,43 @@ module InviteMembersHelper
...
@@ -39,4 +39,43 @@ module InviteMembersHelper
{}
{}
end
end
end
end
def
common_invite_modal_dataset
(
source
)
dataset
=
{
id:
source
.
id
,
name:
source
.
name
,
default_access_level:
Gitlab
::
Access
::
GUEST
}
experiment
(
:member_areas_of_focus
,
user:
current_user
)
do
|
e
|
e
.
publish_to_database
e
.
control
{
dataset
.
merge!
(
areas_of_focus_options:
[],
no_selection_areas_of_focus:
[])
}
e
.
candidate
{
dataset
.
merge!
(
areas_of_focus_options:
member_areas_of_focus_options
.
to_json
,
no_selection_areas_of_focus:
[
'no_selection'
])
}
end
dataset
end
private
def
member_areas_of_focus_options
[
{
value:
'Contribute to the codebase'
,
text:
s_
(
'InviteMembersModal|Contribute to the codebase'
)
},
{
value:
'Collaborate on open issues and merge requests'
,
text:
s_
(
'InviteMembersModal|Collaborate on open issues and merge requests'
)
},
{
value:
'Configure CI/CD'
,
text:
s_
(
'InviteMembersModal|Configure CI/CD'
)
},
{
value:
'Configure security features'
,
text:
s_
(
'InviteMembersModal|Configure security features'
)
},
{
value:
'Other'
,
text:
s_
(
'InviteMembersModal|Other'
)
}
]
end
end
end
app/views/groups/_invite_members_modal.html.haml
View file @
fa18ec94
-
return
unless
can_manage_members?
(
group
)
-
return
unless
can_manage_members?
(
group
)
.js-invite-members-modal
{
data:
{
id:
group
.
id
,
.js-invite-members-modal
{
data:
{
is_project:
'false'
,
name:
group
.
name
,
is_project:
'false'
,
access_levels:
GroupMember
.
access_level_roles
.
to_json
,
access_levels:
GroupMember
.
access_level_roles
.
to_json
,
default_access_level:
Gitlab
::
Access
::
GUEST
,
help_link:
help_page_url
(
'user/permissions'
)
}.
merge
(
group_select_data
(
group
)).
merge
(
common_invite_modal_dataset
(
group
))
}
help_link:
help_page_url
(
'user/permissions'
)
}.
merge
(
group_select_data
(
group
))
}
app/views/projects/_invite_members_modal.html.haml
View file @
fa18ec94
-
return
unless
can_import_members?
-
return
unless
can_import_members?
.js-invite-members-modal
{
data:
{
id:
project
.
id
,
.js-invite-members-modal
{
data:
{
is_project:
'true'
,
name:
project
.
name
,
is_project:
'true'
,
access_levels:
ProjectMember
.
access_level_roles
.
to_json
,
access_levels:
ProjectMember
.
access_level_roles
.
to_json
,
default_access_level:
Gitlab
::
Access
::
GUEST
,
help_link:
help_page_url
(
'user/permissions'
)
}.
merge
(
common_invite_modal_dataset
(
project
))
}
help_link:
help_page_url
(
'user/permissions'
)
}
}
config/feature_flags/experiment/member_areas_of_focus.yml
0 → 100644
View file @
fa18ec94
---
name
:
member_areas_of_focus
introduced_by_url
:
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65273
rollout_issue_url
:
https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/406
milestone
:
'
14.2'
type
:
experiment
group
:
group::expansion
default_enabled
:
false
locale/gitlab.pot
View file @
fa18ec94
...
@@ -18032,6 +18032,18 @@ msgstr ""
...
@@ -18032,6 +18032,18 @@ msgstr ""
msgid "InviteMembersModal|Close invite team members"
msgid "InviteMembersModal|Close invite team members"
msgstr ""
msgstr ""
msgid "InviteMembersModal|Collaborate on open issues and merge requests"
msgstr ""
msgid "InviteMembersModal|Configure CI/CD"
msgstr ""
msgid "InviteMembersModal|Configure security features"
msgstr ""
msgid "InviteMembersModal|Contribute to the codebase"
msgstr ""
msgid "InviteMembersModal|GitLab member or email address"
msgid "InviteMembersModal|GitLab member or email address"
msgstr ""
msgstr ""
...
@@ -18047,6 +18059,9 @@ msgstr ""
...
@@ -18047,6 +18059,9 @@ msgstr ""
msgid "InviteMembersModal|Members were successfully added"
msgid "InviteMembersModal|Members were successfully added"
msgstr ""
msgstr ""
msgid "InviteMembersModal|Other"
msgstr ""
msgid "InviteMembersModal|Search for a group to invite"
msgid "InviteMembersModal|Search for a group to invite"
msgstr ""
msgstr ""
...
@@ -18062,6 +18077,9 @@ msgstr ""
...
@@ -18062,6 +18077,9 @@ msgstr ""
msgid "InviteMembersModal|Something went wrong"
msgid "InviteMembersModal|Something went wrong"
msgstr ""
msgstr ""
msgid "InviteMembersModal|What would you like new member(s) to focus on? (optional)"
msgstr ""
msgid "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group."
msgid "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group."
msgstr ""
msgstr ""
...
...
spec/features/groups/members/manage_members_spec.rb
View file @
fa18ec94
...
@@ -84,6 +84,33 @@ RSpec.describe 'Groups > Members > Manage members' do
...
@@ -84,6 +84,33 @@ RSpec.describe 'Groups > Members > Manage members' do
property:
'existing_user'
,
property:
'existing_user'
,
user:
user1
user:
user1
)
)
expect_no_snowplow_event
(
category:
'Members::CreateService'
,
action:
'area_of_focus'
)
end
it
'adds a user to group with area_of_focus'
,
:js
,
:snowplow
,
:aggregate_failures
do
stub_experiments
(
member_areas_of_focus: :candidate
)
group
.
add_owner
(
user1
)
visit
group_group_members_path
(
group
)
invite_member
(
user2
.
name
,
role:
'Reporter'
,
area_of_focus:
true
)
wait_for_requests
expect_snowplow_event
(
category:
'Members::CreateService'
,
action:
'area_of_focus'
,
label:
'Contribute to the codebase'
,
property:
group
.
members
.
last
.
id
.
to_s
)
expect_snowplow_event
(
category:
'Members::CreateService'
,
action:
'area_of_focus'
,
label:
'Collaborate on open issues and merge requests'
,
property:
group
.
members
.
last
.
id
.
to_s
)
end
end
it
'do not disclose email addresses'
,
:js
do
it
'do not disclose email addresses'
,
:js
do
...
@@ -193,9 +220,36 @@ RSpec.describe 'Groups > Members > Manage members' do
...
@@ -193,9 +220,36 @@ RSpec.describe 'Groups > Members > Manage members' do
property:
'net_new_user'
,
property:
'net_new_user'
,
user:
user1
user:
user1
)
)
expect_no_snowplow_event
(
category:
'Members::CreateService'
,
action:
'area_of_focus'
)
end
end
end
end
it
'invite user to group with area_of_focus'
,
:js
,
:snowplow
,
:aggregate_failures
do
stub_experiments
(
member_areas_of_focus: :candidate
)
group
.
add_owner
(
user1
)
visit
group_group_members_path
(
group
)
invite_member
(
'test@example.com'
,
role:
'Reporter'
,
area_of_focus:
true
)
wait_for_requests
expect_snowplow_event
(
category:
'Members::InviteService'
,
action:
'area_of_focus'
,
label:
'Contribute to the codebase'
,
property:
group
.
members
.
last
.
id
.
to_s
)
expect_snowplow_event
(
category:
'Members::InviteService'
,
action:
'area_of_focus'
,
label:
'Collaborate on open issues and merge requests'
,
property:
group
.
members
.
last
.
id
.
to_s
)
end
context
'when user is a guest'
do
context
'when user is a guest'
do
before
do
before
do
group
.
add_guest
(
user1
)
group
.
add_guest
(
user1
)
...
...
spec/frontend/invite_members/components/invite_members_modal_spec.js
View file @
fa18ec94
...
@@ -6,6 +6,7 @@ import {
...
@@ -6,6 +6,7 @@ import {
GlSprintf
,
GlSprintf
,
GlLink
,
GlLink
,
GlModal
,
GlModal
,
GlFormCheckboxGroup
,
}
from
'
@gitlab/ui
'
;
}
from
'
@gitlab/ui
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
{
stubComponent
}
from
'
helpers/stub_component
'
;
import
{
stubComponent
}
from
'
helpers/stub_component
'
;
...
@@ -15,7 +16,8 @@ import Api from '~/api';
...
@@ -15,7 +16,8 @@ import Api from '~/api';
import
ExperimentTracking
from
'
~/experimentation/experiment_tracking
'
;
import
ExperimentTracking
from
'
~/experimentation/experiment_tracking
'
;
import
InviteMembersModal
from
'
~/invite_members/components/invite_members_modal.vue
'
;
import
InviteMembersModal
from
'
~/invite_members/components/invite_members_modal.vue
'
;
import
MembersTokenSelect
from
'
~/invite_members/components/members_token_select.vue
'
;
import
MembersTokenSelect
from
'
~/invite_members/components/members_token_select.vue
'
;
import
{
INVITE_MEMBERS_IN_COMMENT
}
from
'
~/invite_members/constants
'
;
import
{
INVITE_MEMBERS_IN_COMMENT
,
MEMBER_AREAS_OF_FOCUS
}
from
'
~/invite_members/constants
'
;
import
eventHub
from
'
~/invite_members/event_hub
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
httpStatus
from
'
~/lib/utils/http_status
'
;
import
httpStatus
from
'
~/lib/utils/http_status
'
;
import
{
apiPaths
,
membersApiResponse
,
invitationsApiResponse
}
from
'
../mock_data/api_responses
'
;
import
{
apiPaths
,
membersApiResponse
,
invitationsApiResponse
}
from
'
../mock_data/api_responses
'
;
...
@@ -32,7 +34,12 @@ const inviteeType = 'members';
...
@@ -32,7 +34,12 @@ const inviteeType = 'members';
const
accessLevels
=
{
Guest
:
10
,
Reporter
:
20
,
Developer
:
30
,
Maintainer
:
40
,
Owner
:
50
};
const
accessLevels
=
{
Guest
:
10
,
Reporter
:
20
,
Developer
:
30
,
Maintainer
:
40
,
Owner
:
50
};
const
defaultAccessLevel
=
10
;
const
defaultAccessLevel
=
10
;
const
inviteSource
=
'
unknown
'
;
const
inviteSource
=
'
unknown
'
;
const
noSelectionAreasOfFocus
=
[
'
no_selection
'
];
const
helpLink
=
'
https://example.com
'
;
const
helpLink
=
'
https://example.com
'
;
const
areasOfFocusOptions
=
[
{
text
:
'
area1
'
,
value
:
'
area1
'
},
{
text
:
'
area2
'
,
value
:
'
area2
'
},
];
const
user1
=
{
id
:
1
,
name
:
'
Name One
'
,
username
:
'
one_1
'
,
avatar_url
:
''
};
const
user1
=
{
id
:
1
,
name
:
'
Name One
'
,
username
:
'
one_1
'
,
avatar_url
:
''
};
const
user2
=
{
id
:
2
,
name
:
'
Name Two
'
,
username
:
'
one_2
'
,
avatar_url
:
''
};
const
user2
=
{
id
:
2
,
name
:
'
Name Two
'
,
username
:
'
one_2
'
,
avatar_url
:
''
};
...
@@ -58,7 +65,9 @@ const createComponent = (data = {}, props = {}) => {
...
@@ -58,7 +65,9 @@ const createComponent = (data = {}, props = {}) => {
isProject
,
isProject
,
inviteeType
,
inviteeType
,
accessLevels
,
accessLevels
,
areasOfFocusOptions
,
defaultAccessLevel
,
defaultAccessLevel
,
noSelectionAreasOfFocus
,
helpLink
,
helpLink
,
...
props
,
...
props
,
},
},
...
@@ -119,6 +128,7 @@ describe('InviteMembersModal', () => {
...
@@ -119,6 +128,7 @@ describe('InviteMembersModal', () => {
const
findMembersFormGroup
=
()
=>
wrapper
.
findByTestId
(
'
members-form-group
'
);
const
findMembersFormGroup
=
()
=>
wrapper
.
findByTestId
(
'
members-form-group
'
);
const
membersFormGroupInvalidFeedback
=
()
=>
findMembersFormGroup
().
props
(
'
invalidFeedback
'
);
const
membersFormGroupInvalidFeedback
=
()
=>
findMembersFormGroup
().
props
(
'
invalidFeedback
'
);
const
findMembersSelect
=
()
=>
wrapper
.
findComponent
(
MembersTokenSelect
);
const
findMembersSelect
=
()
=>
wrapper
.
findComponent
(
MembersTokenSelect
);
const
findAreaofFocusCheckBoxGroup
=
()
=>
wrapper
.
findComponent
(
GlFormCheckboxGroup
);
describe
(
'
rendering the modal
'
,
()
=>
{
describe
(
'
rendering the modal
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
...
@@ -164,6 +174,21 @@ describe('InviteMembersModal', () => {
...
@@ -164,6 +174,21 @@ describe('InviteMembersModal', () => {
});
});
});
});
describe
(
'
rendering the areas_of_focus
'
,
()
=>
{
it
(
'
renders the areas_of_focus checkboxes
'
,
()
=>
{
createComponent
();
expect
(
findAreaofFocusCheckBoxGroup
().
props
(
'
options
'
)).
toBe
(
areasOfFocusOptions
);
expect
(
findAreaofFocusCheckBoxGroup
().
exists
()).
toBe
(
true
);
});
it
(
'
does not render the areas_of_focus checkboxes
'
,
()
=>
{
createComponent
({},
{
areasOfFocusOptions
:
[]
});
expect
(
findAreaofFocusCheckBoxGroup
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
displaying the correct introText
'
,
()
=>
{
describe
(
'
displaying the correct introText
'
,
()
=>
{
describe
(
'
when inviting to a project
'
,
()
=>
{
describe
(
'
when inviting to a project
'
,
()
=>
{
describe
(
'
when inviting members
'
,
()
=>
{
describe
(
'
when inviting members
'
,
()
=>
{
...
@@ -214,6 +239,20 @@ describe('InviteMembersModal', () => {
...
@@ -214,6 +239,20 @@ describe('InviteMembersModal', () => {
"
email 'email@example.com' does not match the allowed domains: example1.org
"
;
"
email 'email@example.com' does not match the allowed domains: example1.org
"
;
const
expectedSyntaxError
=
'
email contains an invalid email address
'
;
const
expectedSyntaxError
=
'
email contains an invalid email address
'
;
it
(
'
calls the API with the expected focus data when an areas_of_focus checkbox is clicked
'
,
()
=>
{
const
spy
=
jest
.
spyOn
(
Api
,
'
addGroupMembersByUserId
'
);
const
expectedFocus
=
[
areasOfFocusOptions
[
0
].
value
];
createComponent
({
newUsersToInvite
:
[
user1
]
});
findAreaofFocusCheckBoxGroup
().
vm
.
$emit
(
'
input
'
,
expectedFocus
);
clickInviteButton
();
expect
(
spy
).
toHaveBeenCalledWith
(
user1
.
id
.
toString
(),
expect
.
objectContaining
({
areas_of_focus
:
expectedFocus
}),
);
});
describe
(
'
when inviting an existing user to group by user ID
'
,
()
=>
{
describe
(
'
when inviting an existing user to group by user ID
'
,
()
=>
{
const
postData
=
{
const
postData
=
{
user_id
:
'
1,2
'
,
user_id
:
'
1,2
'
,
...
@@ -221,6 +260,7 @@ describe('InviteMembersModal', () => {
...
@@ -221,6 +260,7 @@ describe('InviteMembersModal', () => {
expires_at
:
undefined
,
expires_at
:
undefined
,
invite_source
:
inviteSource
,
invite_source
:
inviteSource
,
format
:
'
json
'
,
format
:
'
json
'
,
areas_of_focus
:
noSelectionAreasOfFocus
,
};
};
describe
(
'
when member is added successfully
'
,
()
=>
{
describe
(
'
when member is added successfully
'
,
()
=>
{
...
@@ -230,32 +270,36 @@ describe('InviteMembersModal', () => {
...
@@ -230,32 +270,36 @@ describe('InviteMembersModal', () => {
wrapper
.
vm
.
$toast
=
{
show
:
jest
.
fn
()
};
wrapper
.
vm
.
$toast
=
{
show
:
jest
.
fn
()
};
jest
.
spyOn
(
Api
,
'
addGroupMembersByUserId
'
).
mockResolvedValue
({
data
:
postData
});
jest
.
spyOn
(
Api
,
'
addGroupMembersByUserId
'
).
mockResolvedValue
({
data
:
postData
});
jest
.
spyOn
(
wrapper
.
vm
,
'
showToastMessageSuccess
'
);
jest
.
spyOn
(
wrapper
.
vm
,
'
showToastMessageSuccess
'
);
});
it
(
'
includes the non-default selected areas of focus
'
,
()
=>
{
const
focus
=
[
'
abc
'
];
const
updatedPostData
=
{
...
postData
,
areas_of_focus
:
focus
};
wrapper
.
setData
({
selectedAreasOfFocus
:
focus
});
clickInviteButton
();
clickInviteButton
();
});
it
(
'
sets isLoading on the Invite button when it is clicked
'
,
()
=>
{
expect
(
Api
.
addGroupMembersByUserId
).
toHaveBeenCalledWith
(
id
,
updatedPostData
);
expect
(
findInviteButton
().
props
(
'
loading
'
)).
toBe
(
true
);
});
});
it
(
'
removes isLoading from the Invite button when request completes
'
,
async
()
=>
{
describe
(
'
when triggered from regular mounting
'
,
()
=>
{
await
waitForPromises
();
beforeEach
(()
=>
{
clickInviteButton
();
expect
(
findInviteButton
().
props
(
'
loading
'
)).
toBe
(
false
);
});
});
it
(
'
calls Api addGroupMembersByUserId with the correct params
'
,
async
()
=>
{
it
(
'
sets isLoading on the Invite button when it is clicked
'
,
()
=>
{
await
waitForPromises
;
expect
(
findInviteButton
().
props
(
'
loading
'
)).
toBe
(
true
);
});
it
(
'
calls Api addGroupMembersByUserId with the correct params
'
,
()
=>
{
expect
(
Api
.
addGroupMembersByUserId
).
toHaveBeenCalledWith
(
id
,
postData
);
expect
(
Api
.
addGroupMembersByUserId
).
toHaveBeenCalledWith
(
id
,
postData
);
});
});
it
(
'
displays the successful toastMessage
'
,
async
()
=>
{
it
(
'
displays the successful toastMessage
'
,
()
=>
{
await
waitForPromises
;
expect
(
wrapper
.
vm
.
showToastMessageSuccess
).
toHaveBeenCalled
();
expect
(
wrapper
.
vm
.
showToastMessageSuccess
).
toHaveBeenCalled
();
});
});
});
});
});
describe
(
'
when member is not added successfully
'
,
()
=>
{
describe
(
'
when member is not added successfully
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
...
@@ -353,6 +397,7 @@ describe('InviteMembersModal', () => {
...
@@ -353,6 +397,7 @@ describe('InviteMembersModal', () => {
expires_at
:
undefined
,
expires_at
:
undefined
,
email
:
'
email@example.com
'
,
email
:
'
email@example.com
'
,
invite_source
:
inviteSource
,
invite_source
:
inviteSource
,
areas_of_focus
:
noSelectionAreasOfFocus
,
format
:
'
json
'
,
format
:
'
json
'
,
};
};
...
@@ -363,7 +408,20 @@ describe('InviteMembersModal', () => {
...
@@ -363,7 +408,20 @@ describe('InviteMembersModal', () => {
wrapper
.
vm
.
$toast
=
{
show
:
jest
.
fn
()
};
wrapper
.
vm
.
$toast
=
{
show
:
jest
.
fn
()
};
jest
.
spyOn
(
Api
,
'
inviteGroupMembersByEmail
'
).
mockResolvedValue
({
data
:
postData
});
jest
.
spyOn
(
Api
,
'
inviteGroupMembersByEmail
'
).
mockResolvedValue
({
data
:
postData
});
jest
.
spyOn
(
wrapper
.
vm
,
'
showToastMessageSuccess
'
);
jest
.
spyOn
(
wrapper
.
vm
,
'
showToastMessageSuccess
'
);
});
it
(
'
includes the non-default selected areas of focus
'
,
()
=>
{
const
focus
=
[
'
abc
'
];
const
updatedPostData
=
{
...
postData
,
areas_of_focus
:
focus
};
wrapper
.
setData
({
selectedAreasOfFocus
:
focus
});
clickInviteButton
();
expect
(
Api
.
inviteGroupMembersByEmail
).
toHaveBeenCalledWith
(
id
,
updatedPostData
);
});
describe
(
'
when triggered from regular mounting
'
,
()
=>
{
beforeEach
(()
=>
{
clickInviteButton
();
clickInviteButton
();
});
});
...
@@ -375,6 +433,7 @@ describe('InviteMembersModal', () => {
...
@@ -375,6 +433,7 @@ describe('InviteMembersModal', () => {
expect
(
wrapper
.
vm
.
showToastMessageSuccess
).
toHaveBeenCalled
();
expect
(
wrapper
.
vm
.
showToastMessageSuccess
).
toHaveBeenCalled
();
});
});
});
});
});
describe
(
'
when invites are not sent successfully
'
,
()
=>
{
describe
(
'
when invites are not sent successfully
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
...
@@ -465,6 +524,7 @@ describe('InviteMembersModal', () => {
...
@@ -465,6 +524,7 @@ describe('InviteMembersModal', () => {
access_level
:
defaultAccessLevel
,
access_level
:
defaultAccessLevel
,
expires_at
:
undefined
,
expires_at
:
undefined
,
invite_source
:
inviteSource
,
invite_source
:
inviteSource
,
areas_of_focus
:
noSelectionAreasOfFocus
,
format
:
'
json
'
,
format
:
'
json
'
,
};
};
...
@@ -501,7 +561,7 @@ describe('InviteMembersModal', () => {
...
@@ -501,7 +561,7 @@ describe('InviteMembersModal', () => {
});
});
it
(
'
calls Apis with the invite source passed through to openModal
'
,
()
=>
{
it
(
'
calls Apis with the invite source passed through to openModal
'
,
()
=>
{
wrapper
.
vm
.
openModal
(
{
inviteeType
:
'
members
'
,
source
:
'
_invite_source_
'
});
eventHub
.
$emit
(
'
openModal
'
,
{
inviteeType
:
'
members
'
,
source
:
'
_invite_source_
'
});
clickInviteButton
();
clickInviteButton
();
...
@@ -579,9 +639,7 @@ describe('InviteMembersModal', () => {
...
@@ -579,9 +639,7 @@ describe('InviteMembersModal', () => {
clickInviteButton
();
clickInviteButton
();
});
});
it
(
'
displays the generic error message
'
,
async
()
=>
{
it
(
'
displays the generic error message
'
,
()
=>
{
await
waitForPromises
();
expect
(
membersFormGroupInvalidFeedback
()).
toBe
(
'
Something went wrong
'
);
expect
(
membersFormGroupInvalidFeedback
()).
toBe
(
'
Something went wrong
'
);
});
});
});
});
...
@@ -596,7 +654,7 @@ describe('InviteMembersModal', () => {
...
@@ -596,7 +654,7 @@ describe('InviteMembersModal', () => {
});
});
it
(
'
tracks the invite
'
,
()
=>
{
it
(
'
tracks the invite
'
,
()
=>
{
wrapper
.
vm
.
openModal
(
{
inviteeType
:
'
members
'
,
source
:
INVITE_MEMBERS_IN_COMMENT
});
eventHub
.
$emit
(
'
openModal
'
,
{
inviteeType
:
'
members
'
,
source
:
INVITE_MEMBERS_IN_COMMENT
});
clickInviteButton
();
clickInviteButton
();
...
@@ -605,19 +663,37 @@ describe('InviteMembersModal', () => {
...
@@ -605,19 +663,37 @@ describe('InviteMembersModal', () => {
});
});
it
(
'
does not track invite for unknown source
'
,
()
=>
{
it
(
'
does not track invite for unknown source
'
,
()
=>
{
wrapper
.
vm
.
openModal
(
{
inviteeType
:
'
members
'
,
source
:
'
unknown
'
});
eventHub
.
$emit
(
'
openModal
'
,
{
inviteeType
:
'
members
'
,
source
:
'
unknown
'
});
clickInviteButton
();
clickInviteButton
();
expect
(
ExperimentTracking
).
not
.
toHaveBeenCalled
(
);
expect
(
ExperimentTracking
).
not
.
toHaveBeenCalled
With
(
INVITE_MEMBERS_IN_COMMENT
);
});
});
it
(
'
does not track invite undefined source
'
,
()
=>
{
it
(
'
does not track invite undefined source
'
,
()
=>
{
wrapper
.
vm
.
openModal
(
{
inviteeType
:
'
members
'
});
eventHub
.
$emit
(
'
openModal
'
,
{
inviteeType
:
'
members
'
});
clickInviteButton
();
clickInviteButton
();
expect
(
ExperimentTracking
).
not
.
toHaveBeenCalled
();
expect
(
ExperimentTracking
).
not
.
toHaveBeenCalledWith
(
INVITE_MEMBERS_IN_COMMENT
);
});
it
(
'
tracks the view for areas_of_focus
'
,
()
=>
{
eventHub
.
$emit
(
'
openModal
'
,
{
inviteeType
:
'
members
'
});
expect
(
ExperimentTracking
).
toHaveBeenCalledWith
(
MEMBER_AREAS_OF_FOCUS
.
name
);
expect
(
ExperimentTracking
.
prototype
.
event
).
toHaveBeenCalledWith
(
MEMBER_AREAS_OF_FOCUS
.
view
);
});
it
(
'
tracks the invite for areas_of_focus
'
,
()
=>
{
eventHub
.
$emit
(
'
openModal
'
,
{
inviteeType
:
'
members
'
});
clickInviteButton
();
expect
(
ExperimentTracking
).
toHaveBeenCalledWith
(
MEMBER_AREAS_OF_FOCUS
.
name
);
expect
(
ExperimentTracking
.
prototype
.
event
).
toHaveBeenCalledWith
(
MEMBER_AREAS_OF_FOCUS
.
submit
,
);
});
});
});
});
});
});
...
...
spec/helpers/invite_members_helper_spec.rb
View file @
fa18ec94
...
@@ -14,6 +14,56 @@ RSpec.describe InviteMembersHelper do
...
@@ -14,6 +14,56 @@ RSpec.describe InviteMembersHelper do
helper
.
extend
(
Gitlab
::
Experimentation
::
ControllerConcern
)
helper
.
extend
(
Gitlab
::
Experimentation
::
ControllerConcern
)
end
end
describe
'#common_invite_modal_dataset'
do
context
'when member_areas_of_focus is enabled'
,
:experiment
do
context
'with control experience'
do
before
do
stub_experiments
(
member_areas_of_focus: :control
)
end
it
'has expected attributes'
do
attributes
=
{
areas_of_focus_options:
[],
no_selection_areas_of_focus:
[]
}
expect
(
helper
.
common_invite_modal_dataset
(
project
)).
to
include
(
attributes
)
end
end
context
'with candidate experience'
do
before
do
stub_experiments
(
member_areas_of_focus: :candidate
)
end
it
'has expected attributes'
,
:aggregate_failures
do
output
=
helper
.
common_invite_modal_dataset
(
project
)
expect
(
output
[
:no_selection_areas_of_focus
]).
to
eq
[
'no_selection'
]
expect
(
Gitlab
::
Json
.
parse
(
output
[
:areas_of_focus_options
]).
first
[
'value'
]).
to
eq
'Contribute to the codebase'
end
end
end
context
'when member_areas_of_focus is disabled'
do
before
do
stub_feature_flags
(
member_areas_of_focus:
false
)
end
it
'has expected attributes'
do
attributes
=
{
id:
project
.
id
,
name:
project
.
name
,
default_access_level:
Gitlab
::
Access
::
GUEST
,
areas_of_focus_options:
[],
no_selection_areas_of_focus:
[]
}
expect
(
helper
.
common_invite_modal_dataset
(
project
)).
to
match
(
attributes
)
end
end
end
context
'with project'
do
context
'with project'
do
before
do
before
do
allow
(
helper
).
to
receive
(
:current_user
)
{
owner
}
allow
(
helper
).
to
receive
(
:current_user
)
{
owner
}
...
...
spec/support/helpers/features/invite_members_modal_helper.rb
View file @
fa18ec94
...
@@ -5,7 +5,7 @@ module Spec
...
@@ -5,7 +5,7 @@ module Spec
module
Helpers
module
Helpers
module
Features
module
Features
module
InviteMembersModalHelper
module
InviteMembersModalHelper
def
invite_member
(
name
,
role:
'Guest'
,
expires_at:
nil
)
def
invite_member
(
name
,
role:
'Guest'
,
expires_at:
nil
,
area_of_focus:
false
)
click_on
'Invite members'
click_on
'Invite members'
page
.
within
'#invite-members-modal'
do
page
.
within
'#invite-members-modal'
do
...
@@ -14,6 +14,7 @@ module Spec
...
@@ -14,6 +14,7 @@ module Spec
wait_for_requests
wait_for_requests
click_button
name
click_button
name
choose_options
(
role
,
expires_at
)
choose_options
(
role
,
expires_at
)
choose_area_of_focus
if
area_of_focus
click_button
'Invite'
click_button
'Invite'
...
@@ -41,7 +42,14 @@ module Spec
...
@@ -41,7 +42,14 @@ module Spec
click_button
role
click_button
role
end
end
fill_in
'YYYY-MM-DD'
,
with:
expires_at
.
try
(
:strftime
,
'%Y-%m-%d'
)
fill_in
'YYYY-MM-DD'
,
with:
expires_at
.
strftime
(
'%Y-%m-%d'
)
if
expires_at
end
def
choose_area_of_focus
page
.
within
'[data-testid="area-of-focus-checks"]'
do
check
'Contribute to the codebase'
check
'Collaborate on open issues and merge requests'
end
end
end
end
end
end
end
...
...
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