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
3e773ae5
Commit
3e773ae5
authored
Mar 23, 2022
by
Jiaan Louw
Committed by
Phil Hughes
Mar 23, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Remove delete user modal HAML code
The modal strings will be moved to Vue.
parent
f0c53b2b
Changes
19
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
213 additions
and
640 deletions
+213
-640
app/assets/javascripts/admin/users/components/actions/delete.vue
...ets/javascripts/admin/users/components/actions/delete.vue
+28
-11
app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
...in/users/components/actions/delete_with_contributions.vue
+28
-11
app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue
.../users/components/actions/shared/shared_delete_action.vue
+0
-52
app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
...ripts/admin/users/components/modals/delete_user_modal.vue
+47
-65
app/assets/javascripts/admin/users/components/modals/delete_user_modal_event_hub.js
...in/users/components/modals/delete_user_modal_event_hub.js
+5
-0
app/assets/javascripts/admin/users/components/modals/user_modal_manager.vue
...ipts/admin/users/components/modals/user_modal_manager.vue
+0
-77
app/assets/javascripts/admin/users/constants.js
app/assets/javascripts/admin/users/constants.js
+0
-6
app/assets/javascripts/admin/users/index.js
app/assets/javascripts/admin/users/index.js
+6
-41
app/views/admin/identities/index.html.haml
app/views/admin/identities/index.html.haml
+0
-2
app/views/admin/impersonation_tokens/index.html.haml
app/views/admin/impersonation_tokens/index.html.haml
+0
-2
app/views/admin/users/_modals.html.haml
app/views/admin/users/_modals.html.haml
+0
-20
app/views/admin/users/_users.html.haml
app/views/admin/users/_users.html.haml
+0
-2
app/views/admin/users/keys.html.haml
app/views/admin/users/keys.html.haml
+0
-1
app/views/admin/users/projects.html.haml
app/views/admin/users/projects.html.haml
+0
-2
app/views/admin/users/show.html.haml
app/views/admin/users/show.html.haml
+0
-1
spec/frontend/admin/users/components/actions/actions_spec.js
spec/frontend/admin/users/components/actions/actions_spec.js
+21
-21
spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
...nents/modals/__snapshots__/delete_user_modal_spec.js.snap
+21
-153
spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
...d/admin/users/components/modals/delete_user_modal_spec.js
+57
-47
spec/frontend/admin/users/components/modals/user_modal_manager_spec.js
.../admin/users/components/modals/user_modal_manager_spec.js
+0
-126
No files found.
app/assets/javascripts/admin/users/components/actions/delete.vue
View file @
3e773ae5
<
script
>
import
SharedDeleteAction
from
'
./shared/shared_delete_action.vue
'
;
import
{
GlDropdownItem
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
~/locale
'
;
import
eventHub
,
{
EVENT_OPEN_DELETE_USER_MODAL
}
from
'
../modals/delete_user_modal_event_hub
'
;
export
default
{
components
:
{
SharedDeleteAction
,
GlDropdownItem
,
},
props
:
{
username
:
{
...
...
@@ -20,17 +22,32 @@ export default {
default
:
()
=>
[],
},
},
methods
:
{
onClick
()
{
const
{
username
,
paths
,
userDeletionObstacles
}
=
this
;
eventHub
.
$emit
(
EVENT_OPEN_DELETE_USER_MODAL
,
{
username
,
blockPath
:
paths
.
block
,
deletePath
:
paths
.
delete
,
userDeletionObstacles
,
i18n
:
{
title
:
s__
(
'
AdminUsers|Delete User %{username}?
'
),
primaryButtonLabel
:
s__
(
'
AdminUsers|Delete user
'
),
messageBody
:
s__
(
`AdminUsers|You are about to permanently delete the user %{username}. Issues, merge requests,
and groups linked to them will be transferred to a system-wide "Ghost-user". To avoid data loss,
consider using the %{strongStart}block user%{strongEnd} feature instead. Once you %{strongStart}Delete user%{strongEnd},
it cannot be undone or recovered.`
),
},
});
},
},
};
</
script
>
<
template
>
<shared-delete-action
modal-type=
"delete"
:username=
"username"
:paths=
"paths"
:delete-path=
"paths.delete"
:user-deletion-obstacles=
"userDeletionObstacles"
>
<slot></slot>
</shared-delete-action>
<gl-dropdown-item
@
click=
"onClick"
>
<span
class=
"gl-text-red-500"
>
<slot></slot>
</span>
</gl-dropdown-item>
</
template
>
app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
View file @
3e773ae5
<
script
>
import
SharedDeleteAction
from
'
./shared/shared_delete_action.vue
'
;
import
{
GlDropdownItem
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
~/locale
'
;
import
eventHub
,
{
EVENT_OPEN_DELETE_USER_MODAL
}
from
'
../modals/delete_user_modal_event_hub
'
;
export
default
{
components
:
{
SharedDeleteAction
,
GlDropdownItem
,
},
props
:
{
username
:
{
...
...
@@ -20,17 +22,32 @@ export default {
default
:
()
=>
[],
},
},
methods
:
{
onClick
()
{
const
{
username
,
paths
,
userDeletionObstacles
}
=
this
;
eventHub
.
$emit
(
EVENT_OPEN_DELETE_USER_MODAL
,
{
username
,
blockPath
:
paths
.
block
,
deletePath
:
paths
.
deleteWithContributions
,
userDeletionObstacles
,
i18n
:
{
title
:
s__
(
'
AdminUsers|Delete User %{username} and contributions?
'
),
primaryButtonLabel
:
s__
(
'
AdminUsers|Delete user and contributions
'
),
messageBody
:
s__
(
`AdminUsers|You are about to permanently delete the user %{username}. This will delete all of the issues,
merge requests, and groups linked to them. To avoid data loss,
consider using the %{strongStart}block user%{strongEnd} feature instead. Once you %{strongStart}Delete user%{strongEnd},
it cannot be undone or recovered.`
),
},
});
},
},
};
</
script
>
<
template
>
<shared-delete-action
modal-type=
"delete-with-contributions"
:username=
"username"
:paths=
"paths"
:delete-path=
"paths.deleteWithContributions"
:user-deletion-obstacles=
"userDeletionObstacles"
>
<slot></slot>
</shared-delete-action>
<gl-dropdown-item
@
click=
"onClick"
>
<span
class=
"gl-text-red-500"
>
<slot></slot>
</span>
</gl-dropdown-item>
</
template
>
app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue
deleted
100644 → 0
View file @
f0c53b2b
<
script
>
import
{
GlDropdownItem
}
from
'
@gitlab/ui
'
;
export
default
{
components
:
{
GlDropdownItem
,
},
props
:
{
username
:
{
type
:
String
,
required
:
true
,
},
paths
:
{
type
:
Object
,
required
:
true
,
},
deletePath
:
{
type
:
String
,
required
:
true
,
},
modalType
:
{
type
:
String
,
required
:
true
,
},
userDeletionObstacles
:
{
type
:
Array
,
required
:
true
,
},
},
computed
:
{
modalAttributes
()
{
return
{
'
data-block-user-url
'
:
this
.
paths
.
block
,
'
data-delete-user-url
'
:
this
.
deletePath
,
'
data-gl-modal-action
'
:
this
.
modalType
,
'
data-username
'
:
this
.
username
,
'
data-user-deletion-obstacles
'
:
JSON
.
stringify
(
this
.
userDeletionObstacles
),
};
},
},
};
</
script
>
<
template
>
<div
class=
"js-delete-user-modal-button"
v-bind=
"
{ ...modalAttributes }">
<gl-dropdown-item>
<span
class=
"gl-text-red-500"
>
<slot></slot>
</span>
</gl-dropdown-item>
</div>
</
template
>
app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
View file @
3e773ae5
<
script
>
import
{
GlModal
,
GlButton
,
GlFormInput
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
*
as
Sentry
from
'
@sentry/browser
'
;
import
{
s__
,
sprintf
}
from
'
~/locale
'
;
import
UserDeletionObstaclesList
from
'
~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue
'
;
import
eventHub
,
{
EVENT_OPEN_DELETE_USER_MODAL
}
from
'
./delete_user_modal_event_hub
'
;
export
default
{
components
:
{
...
...
@@ -13,47 +13,23 @@ export default {
UserDeletionObstaclesList
,
},
props
:
{
title
:
{
type
:
String
,
required
:
true
,
},
content
:
{
type
:
String
,
required
:
true
,
},
action
:
{
type
:
String
,
required
:
true
,
},
secondaryAction
:
{
type
:
String
,
required
:
true
,
},
deleteUserUrl
:
{
type
:
String
,
required
:
true
,
},
blockUserUrl
:
{
type
:
String
,
required
:
true
,
},
username
:
{
type
:
String
,
required
:
true
,
},
csrfToken
:
{
type
:
String
,
required
:
true
,
},
userDeletionObstacles
:
{
type
:
String
,
required
:
false
,
default
:
'
[]
'
,
},
},
data
()
{
return
{
enteredUsername
:
''
,
username
:
''
,
blockPath
:
''
,
deletePath
:
''
,
userDeletionObstacles
:
[],
i18n
:
{
title
:
''
,
primaryButtonLabel
:
''
,
messageBody
:
''
,
},
};
},
computed
:
{
...
...
@@ -61,75 +37,80 @@ export default {
return
this
.
username
.
trim
();
},
modalTitle
()
{
return
sprintf
(
this
.
title
,
{
username
:
this
.
trimmedUsername
},
false
);
},
secondaryButtonLabel
()
{
return
s__
(
'
AdminUsers|Block user
'
);
return
sprintf
(
this
.
i18n
.
title
,
{
username
:
this
.
trimmedUsername
},
false
);
},
canSubmit
()
{
return
this
.
enteredUsername
===
this
.
trimmedUsername
;
return
this
.
enteredUsername
&&
this
.
enteredUsername
===
this
.
trimmedUsername
;
},
obstacles
()
{
try
{
return
JSON
.
parse
(
this
.
userDeletionObstacles
);
}
catch
(
e
)
{
Sentry
.
captureException
(
e
);
}
return
[];
secondaryButtonLabel
()
{
return
s__
(
'
AdminUsers|Block user
'
);
},
},
mounted
()
{
eventHub
.
$on
(
EVENT_OPEN_DELETE_USER_MODAL
,
this
.
onOpenEvent
);
},
destroyed
()
{
eventHub
.
$off
(
EVENT_OPEN_DELETE_USER_MODAL
,
this
.
onOpenEvent
);
},
methods
:
{
show
()
{
onOpenEvent
({
username
,
blockPath
,
deletePath
,
userDeletionObstacles
,
i18n
})
{
this
.
username
=
username
;
this
.
blockPath
=
blockPath
;
this
.
deletePath
=
deletePath
;
this
.
userDeletionObstacles
=
userDeletionObstacles
;
this
.
i18n
=
i18n
;
this
.
openModal
();
},
openModal
()
{
this
.
$refs
.
modal
.
show
();
},
onSubmit
()
{
this
.
$refs
.
form
.
submit
();
this
.
enteredUsername
=
''
;
},
onCancel
()
{
this
.
enteredUsername
=
''
;
this
.
$refs
.
modal
.
hide
();
},
onSecondaryAction
()
{
const
{
form
}
=
this
.
$refs
;
form
.
action
=
this
.
blockUserUrl
;
form
.
action
=
this
.
blockPath
;
this
.
$refs
.
method
.
value
=
'
put
'
;
form
.
submit
();
},
onSubmit
()
{
this
.
$refs
.
form
.
submit
();
this
.
enteredUsername
=
''
;
},
},
};
</
script
>
<
template
>
<gl-modal
ref=
"modal"
modal-id=
"delete-user-modal"
:title=
"modalTitle"
kind=
"danger"
>
<p>
<gl-sprintf
:message=
"
content
"
>
<gl-sprintf
:message=
"
i18n.messageBody
"
>
<template
#username
>
<strong>
{{
trimmedUsername
}}
</strong>
<strong
data-testid=
"message-username"
>
{{
trimmedUsername
}}
</strong>
</
template
>
<
template
#strong=
"
props
"
>
<strong>
{{
props
.
content
}}
</strong>
<
template
#strong=
"
{ content }
"
>
<strong>
{{
content
}}
</strong>
</
template
>
</gl-sprintf>
</p>
<user-deletion-obstacles-list
v-if=
"
o
bstacles.length"
:obstacles=
"
o
bstacles"
v-if=
"
userDeletionO
bstacles.length"
:obstacles=
"
userDeletionO
bstacles"
:user-name=
"trimmedUsername"
/>
<p>
<gl-sprintf
:message=
"s__('AdminUsers|To confirm, type %{username}')"
>
<
template
#username
>
<code
class=
"gl-white-space-pre-wrap"
>
{{
trimmedUsername
}}
</code>
<code
data-testid=
"confirm-username"
class=
"gl-white-space-pre-wrap"
>
{{
trimmedUsername
}}
</code>
</
template
>
</gl-sprintf>
</p>
<form
ref=
"form"
:action=
"delete
UserUrl
"
method=
"post"
@
submit
.
prevent
>
<form
ref=
"form"
:action=
"delete
Path
"
method=
"post"
@
submit
.
prevent
>
<input
ref=
"method"
type=
"hidden"
name=
"_method"
value=
"delete"
/>
<input
:value=
"csrfToken"
type=
"hidden"
name=
"authenticity_token"
/>
<gl-form-input
...
...
@@ -140,6 +121,7 @@ export default {
autocomplete=
"off"
/>
</form>
<
template
#modal-footer
>
<gl-button
@
click=
"onCancel"
>
{{
__
(
'
Cancel
'
)
}}
</gl-button>
<gl-button
...
...
@@ -148,10 +130,10 @@ export default {
variant=
"danger"
@
click=
"onSecondaryAction"
>
{{
secondary
Action
}}
{{
secondary
ButtonLabel
}}
</gl-button>
<gl-button
:disabled=
"!canSubmit"
category=
"primary"
variant=
"danger"
@
click=
"onSubmit"
>
{{
action
i18n
.
primaryButtonLabel
}}
</gl-button>
</
template
>
</gl-modal>
...
...
app/assets/javascripts/admin/users/components/modals/delete_user_modal_event_hub.js
0 → 100644
View file @
3e773ae5
import
createEventHub
from
'
~/helpers/event_hub_factory
'
;
export
default
createEventHub
();
export
const
EVENT_OPEN_DELETE_USER_MODAL
=
Symbol
(
'
OPEN
'
);
app/assets/javascripts/admin/users/components/modals/user_modal_manager.vue
deleted
100644 → 0
View file @
f0c53b2b
<
script
>
import
DeleteUserModal
from
'
./delete_user_modal.vue
'
;
export
default
{
components
:
{
DeleteUserModal
},
props
:
{
modalConfiguration
:
{
required
:
true
,
type
:
Object
,
},
csrfToken
:
{
required
:
true
,
type
:
String
,
},
selector
:
{
required
:
true
,
type
:
String
,
},
},
data
()
{
return
{
currentModalData
:
null
,
};
},
computed
:
{
activeModal
()
{
return
Boolean
(
this
.
currentModalData
);
},
modalProps
()
{
const
{
glModalAction
:
requestedAction
}
=
this
.
currentModalData
;
return
{
...
this
.
modalConfiguration
[
requestedAction
],
...
this
.
currentModalData
,
csrfToken
:
this
.
csrfToken
,
};
},
},
mounted
()
{
/*
* Here we're looking for every button that needs to launch a modal
* on click, and then attaching a click event handler to show the modal
* if it's correctly configured.
*
* TODO: Replace this with integrated modal components https://gitlab.com/gitlab-org/gitlab/-/issues/320922
*/
document
.
querySelectorAll
(
this
.
selector
).
forEach
((
button
)
=>
{
button
.
addEventListener
(
'
click
'
,
(
e
)
=>
{
if
(
!
button
.
dataset
.
glModalAction
)
return
;
e
.
preventDefault
();
this
.
show
(
button
.
dataset
);
});
});
},
methods
:
{
show
(
modalData
)
{
const
{
glModalAction
:
requestedAction
}
=
modalData
;
if
(
!
this
.
modalConfiguration
[
requestedAction
])
{
throw
new
Error
(
`Modal action
${
requestedAction
}
has no configuration in HTML`
);
}
this
.
currentModalData
=
modalData
;
return
this
.
$nextTick
().
then
(()
=>
{
this
.
$refs
.
modal
.
show
();
});
},
},
};
</
script
>
<
template
>
<delete-user-modal
v-if=
"activeModal"
ref=
"modal"
v-bind=
"modalProps"
/>
</
template
>
app/assets/javascripts/admin/users/constants.js
View file @
3e773ae5
...
...
@@ -20,9 +20,3 @@ export const I18N_USER_ACTIONS = {
ban
:
s__
(
'
AdminUsers|Ban user
'
),
unban
:
s__
(
'
AdminUsers|Unban user
'
),
};
export
const
CONFIRM_DELETE_BUTTON_SELECTOR
=
'
.js-delete-user-modal-button
'
;
export
const
MODAL_TEXTS_CONTAINER_SELECTOR
=
'
#js-modal-texts
'
;
export
const
MODAL_MANAGER_SELECTOR
=
'
#js-delete-user-modal
'
;
app/assets/javascripts/admin/users/index.js
View file @
3e773ae5
...
...
@@ -4,13 +4,8 @@ import createDefaultClient from '~/lib/graphql';
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
csrf
from
'
~/lib/utils/csrf
'
;
import
AdminUsersApp
from
'
./components/app.vue
'
;
import
ModalManager
from
'
./components/modals/user_modal_manager
.vue
'
;
import
DeleteUserModal
from
'
./components/modals/delete_user_modal
.vue
'
;
import
UserActions
from
'
./components/user_actions.vue
'
;
import
{
CONFIRM_DELETE_BUTTON_SELECTOR
,
MODAL_TEXTS_CONTAINER_SELECTOR
,
MODAL_MANAGER_SELECTOR
,
}
from
'
./constants
'
;
Vue
.
use
(
VueApollo
);
...
...
@@ -46,43 +41,13 @@ export const initAdminUserActions = (el = document.querySelector('#js-admin-user
initApp
(
el
,
UserActions
,
'
user
'
,
{
showButtonLabels
:
true
});
export
const
initDeleteUserModals
=
()
=>
{
const
modalsMountElement
=
document
.
querySelector
(
MODAL_TEXTS_CONTAINER_SELECTOR
);
if
(
!
modalsMountElement
)
{
return
;
}
const
modalConfiguration
=
Array
.
from
(
modalsMountElement
.
children
).
reduce
((
accumulator
,
node
)
=>
{
const
{
modal
,
...
config
}
=
node
.
dataset
;
return
{
...
accumulator
,
[
modal
]:
{
title
:
node
.
dataset
.
title
,
...
config
,
content
:
node
.
innerHTML
,
},
};
},
{});
// eslint-disable-next-line no-new
new
Vue
({
el
:
MODAL_MANAGER_SELECTOR
,
return
new
Vue
({
functional
:
true
,
methods
:
{
show
(...
args
)
{
this
.
$refs
.
manager
.
show
(...
args
);
},
},
render
(
h
)
{
return
h
(
ModalManager
,
{
ref
:
'
manager
'
,
render
:
(
createElement
)
=>
createElement
(
DeleteUserModal
,
{
props
:
{
selector
:
CONFIRM_DELETE_BUTTON_SELECTOR
,
modalConfiguration
,
csrfToken
:
csrf
.
token
,
},
});
},
});
}),
}).
$mount
();
};
app/views/admin/identities/index.html.haml
View file @
3e773ae5
...
...
@@ -15,5 +15,3 @@
=
render
@identities
-
else
%h4
=
_
(
'This user has no identities'
)
=
render
partial:
'admin/users/modals'
app/views/admin/impersonation_tokens/index.html.haml
View file @
3e773ae5
...
...
@@ -28,5 +28,3 @@
impersonation:
true
,
active_tokens:
@active_impersonation_tokens
,
revoke_route_helper:
->
(
token
)
{
revoke_admin_user_impersonation_token_path
(
token
.
user
,
token
)
}
=
render
partial:
'admin/users/modals'
app/views/admin/users/_modals.html.haml
deleted
100644 → 0
View file @
f0c53b2b
#js-delete-user-modal
#js-modal-texts
.hidden
{
"hidden"
:
true
,
"aria-hidden"
:
"true"
}
%div
{
data:
{
modal:
"delete"
,
title:
s_
(
"AdminUsers|Delete User %{username}?"
),
action:
s_
(
'AdminUsers|Delete user'
),
'secondary-action'
:
s_
(
'AdminUsers|Block user'
)
}
}
=
s_
(
'
AdminUsers
|
You
are
about
to
permanently
delete
the
user
%{username}
.
Issues
,
merge
requests
,
and
groups
linked
to
them
will
be
transferred
to
a
system
-
wide
"Ghost-user"
.
To
avoid
data
loss
,
consider
using
the
%{strongStart}
block
user
%{strongEnd}
feature
instead
.
Once
you
%{strongStart}
Delete
user
%{strongEnd}
,
it
cannot
be
undone
or
recovered
.
'
)
%div
{
data:
{
modal:
"delete-with-contributions"
,
title:
s_
(
"AdminUsers|Delete User %{username} and contributions?"
),
action:
s_
(
'AdminUsers|Delete user and contributions'
)
,
'secondary-action'
:
s_
(
'AdminUsers|Block user'
)
}
}
=
s_
(
'
AdminUsers
|
You
are
about
to
permanently
delete
the
user
%{username}
.
This
will
delete
all
of
the
issues
,
merge
requests
,
and
groups
linked
to
them
.
To
avoid
data
loss
,
consider
using
the
%{strongStart}
block
user
%{strongEnd}
feature
instead
.
Once
you
%{strongStart}
Delete
user
%{strongEnd}
,
it
cannot
be
undone
or
recovered
.
'
)
app/views/admin/users/_users.html.haml
View file @
3e773ae5
...
...
@@ -68,5 +68,3 @@
=
gl_loading_icon
(
size:
'lg'
,
css_class:
'gl-my-7'
)
=
paginate_collection
@users
=
render
partial:
'admin/users/modals'
app/views/admin/users/keys.html.haml
View file @
3e773ae5
...
...
@@ -3,4 +3,3 @@
-
page_title
_
(
"SSH Keys"
),
@user
.
name
,
_
(
"Users"
)
=
render
'admin/users/head'
=
render
'profiles/keys/key_table'
,
admin:
true
=
render
partial:
'admin/users/modals'
app/views/admin/users/projects.html.haml
View file @
3e773ae5
...
...
@@ -48,5 +48,3 @@
-
if
member
.
respond_to?
:project
=
link_to
project_project_member_path
(
project
,
member
),
data:
{
confirm:
remove_member_message
(
member
),
confirm_btn_variant:
'danger'
},
aria:
{
label:
_
(
'Remove'
)
},
remote:
true
,
method: :delete
,
class:
"btn btn-sm btn-danger gl-button btn-icon gl-ml-3"
,
title:
_
(
'Remove user from project'
)
do
=
sprite_icon
(
'remove'
,
size:
16
,
css_class:
'gl-icon'
)
=
render
partial:
'admin/users/modals'
app/views/admin/users/show.html.haml
View file @
3e773ae5
...
...
@@ -146,4 +146,3 @@
.col-md-6.gl-display-none.gl-md-display-block
=
render
'admin/users/profile'
,
user:
@user
=
render
'admin/users/user_detail_note'
=
render
partial:
'admin/users/modals'
spec/frontend/admin/users/components/actions/actions_spec.js
View file @
3e773ae5
import
{
GlDropdownItem
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
nextTick
}
from
'
vue
'
;
import
{
kebabCase
}
from
'
lodash
'
;
import
Actions
from
'
~/admin/users/components/actions
'
;
import
SharedDeleteAction
from
'
~/admin/users/components/actions/shared/shared_delete_action.vue
'
;
import
eventHub
,
{
EVENT_OPEN_DELETE_USER_MODAL
,
}
from
'
~/admin/users/components/modals/delete_user_modal_event_hub
'
;
import
{
capitalizeFirstCharacter
}
from
'
~/lib/utils/text_utility
'
;
import
{
OBSTACLE_TYPES
}
from
'
~/vue_shared/components/user_deletion_obstacles/constants
'
;
import
{
CONFIRMATION_ACTIONS
,
DELETE_ACTIONS
}
from
'
../../constants
'
;
...
...
@@ -14,12 +14,11 @@ describe('Action components', () => {
const
findDropdownItem
=
()
=>
wrapper
.
find
(
GlDropdownItem
);
const
initComponent
=
({
component
,
props
,
stubs
=
{}
}
=
{})
=>
{
const
initComponent
=
({
component
,
props
}
=
{})
=>
{
wrapper
=
shallowMount
(
component
,
{
propsData
:
{
...
props
,
},
stubs
,
});
};
...
...
@@ -29,7 +28,7 @@ describe('Action components', () => {
});
describe
(
'
CONFIRMATION_ACTIONS
'
,
()
=>
{
it
.
each
(
CONFIRMATION_ACTIONS
)(
'
renders a dropdown item for "%s"
'
,
async
(
action
)
=>
{
it
.
each
(
CONFIRMATION_ACTIONS
)(
'
renders a dropdown item for "%s"
'
,
(
action
)
=>
{
initComponent
({
component
:
Actions
[
capitalizeFirstCharacter
(
action
)],
props
:
{
...
...
@@ -38,20 +37,23 @@ describe('Action components', () => {
},
});
await
nextTick
();
expect
(
findDropdownItem
().
exists
()).
toBe
(
true
);
});
});
describe
(
'
DELETE_ACTION_COMPONENTS
'
,
()
=>
{
beforeEach
(()
=>
{
jest
.
spyOn
(
eventHub
,
'
$emit
'
).
mockImplementation
();
});
const
userDeletionObstacles
=
[
{
name
:
'
schedule1
'
,
type
:
OBSTACLE_TYPES
.
oncallSchedules
},
{
name
:
'
policy1
'
,
type
:
OBSTACLE_TYPES
.
escalationPolicies
},
];
it
.
each
(
DELETE_ACTIONS
.
map
((
action
)
=>
[
action
,
paths
[
action
]])
)(
'
renders a dropdown item for "%s"
'
,
async
(
action
,
expectedPath
)
=>
{
it
.
each
(
DELETE_ACTIONS
)(
'
renders a dropdown item
that opens the delete user modal when clicked
for "%s"
'
,
async
(
action
)
=>
{
initComponent
({
component
:
Actions
[
capitalizeFirstCharacter
(
action
)],
props
:
{
...
...
@@ -59,21 +61,19 @@ describe('Action components', () => {
paths
,
userDeletionObstacles
,
},
stubs
:
{
SharedDeleteAction
},
});
await
nextTick
();
const
sharedAction
=
wrapper
.
find
(
SharedDeleteAction
);
await
findDropdownItem
().
vm
.
$emit
(
'
click
'
);
expect
(
sharedAction
.
attributes
(
'
data-block-user-url
'
)).
toBe
(
paths
.
block
);
expect
(
sharedAction
.
attributes
(
'
data-delete-user-url
'
)).
toBe
(
expectedPath
);
expect
(
sharedAction
.
attributes
(
'
data-gl-modal-action
'
)).
toBe
(
kebabCase
(
action
));
expect
(
sharedAction
.
attributes
(
'
data-username
'
)).
toBe
(
'
John Doe
'
);
expect
(
sharedAction
.
attributes
(
'
data-user-deletion-obstacles
'
)).
toBe
(
JSON
.
stringify
(
userDeletionObstacles
),
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
EVENT_OPEN_DELETE_USER_MODAL
,
expect
.
objectContaining
({
username
:
'
John Doe
'
,
blockPath
:
paths
.
block
,
deletePath
:
paths
[
action
],
userDeletionObstacles
,
}),
);
expect
(
findDropdownItem
().
exists
()).
toBe
(
true
);
},
);
});
...
...
spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
View file @
3e773ae5
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`User Operation confirmation modal renders modal with form included 1`] = `
<div>
<p>
<gl-sprintf-stub
message="content"
/>
</p>
<user-deletion-obstacles-list-stub
obstacles="schedule1,policy1"
username="username"
exports[`Delete user modal renders modal with form included 1`] = `
<form
action=""
method="post"
>
<input
name="_method"
type="hidden"
value="delete"
/>
<p>
<gl-sprintf-stub
message="To confirm, type %{username}"
/>
</p>
<form
action="delete-url"
method="post"
>
<input
name="_method"
type="hidden"
value="delete"
/>
<input
name="authenticity_token"
type="hidden"
value="csrf"
/>
<gl-form-input-stub
autocomplete="off"
autofocus=""
name="username"
type="text"
value=""
/>
</form>
<gl-button-stub
buttontextclasses=""
category="primary"
icon=""
size="medium"
variant="default"
>
Cancel
</gl-button-stub>
<gl-button-stub
buttontextclasses=""
category="secondary"
disabled="true"
icon=""
size="medium"
variant="danger"
>
secondaryAction
</gl-button-stub>
<gl-button-stub
buttontextclasses=""
category="primary"
disabled="true"
icon=""
size="medium"
variant="danger"
>
action
</gl-button-stub>
</div>
`;
exports[`User Operation confirmation modal when user's name has leading and trailing whitespace displays user's name without whitespace 1`] = `
<div>
<p>
content
</p>
<user-deletion-obstacles-list-stub
obstacles="schedule1,policy1"
username="John Smith"
<input
name="authenticity_token"
type="hidden"
value="csrf"
/>
<p>
To confirm, type
<code
class="gl-white-space-pre-wrap"
>
John Smith
</code>
</p>
<form
action="delete-url"
method="post"
>
<input
name="_method"
type="hidden"
value="delete"
/>
<input
name="authenticity_token"
type="hidden"
value="csrf"
/>
<gl-form-input-stub
autocomplete="off"
autofocus=""
name="username"
type="text"
value=""
/>
</form>
<gl-button-stub
buttontextclasses=""
category="primary"
icon=""
size="medium"
variant="default"
>
Cancel
</gl-button-stub>
<gl-button-stub
buttontextclasses=""
category="secondary"
disabled="true"
icon=""
size="medium"
variant="danger"
>
secondaryAction
</gl-button-stub>
<gl-button-stub
buttontextclasses=""
category="primary"
disabled="true"
icon=""
size="medium"
variant="danger"
>
action
</gl-button-stub>
</div>
<gl-form-input-stub
autocomplete="off"
autofocus=""
name="username"
type="text"
value=""
/>
</form>
`;
spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
View file @
3e773ae5
import
{
GlButton
,
GlFormInput
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
nextTick
}
from
'
vue
'
;
import
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
eventHub
,
{
EVENT_OPEN_DELETE_USER_MODAL
,
}
from
'
~/admin/users/components/modals/delete_user_modal_event_hub
'
;
import
DeleteUserModal
from
'
~/admin/users/components/modals/delete_user_modal.vue
'
;
import
UserDeletionObstaclesList
from
'
~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue
'
;
import
ModalStub
from
'
./stubs/modal_stub
'
;
...
...
@@ -9,7 +11,7 @@ const TEST_DELETE_USER_URL = 'delete-url';
const
TEST_BLOCK_USER_URL
=
'
block-url
'
;
const
TEST_CSRF
=
'
csrf
'
;
describe
(
'
User Operation confirmation
modal
'
,
()
=>
{
describe
(
'
Delete user
modal
'
,
()
=>
{
let
wrapper
;
let
formSubmitSpy
;
...
...
@@ -27,28 +29,36 @@ describe('User Operation confirmation modal', () => {
const
getMethodParam
=
()
=>
new
FormData
(
findForm
().
element
).
get
(
'
_method
'
);
const
getFormAction
=
()
=>
findForm
().
attributes
(
'
action
'
);
const
findUserDeletionObstaclesList
=
()
=>
wrapper
.
findComponent
(
UserDeletionObstaclesList
);
const
findMessageUsername
=
()
=>
wrapper
.
findByTestId
(
'
message-username
'
);
const
findConfirmUsername
=
()
=>
wrapper
.
findByTestId
(
'
confirm-username
'
);
const
emitOpenModalEvent
=
(
modalData
)
=>
{
return
eventHub
.
$emit
(
EVENT_OPEN_DELETE_USER_MODAL
,
modalData
);
};
const
setUsername
=
(
username
)
=>
{
findUsernameInput
().
vm
.
$emit
(
'
input
'
,
username
);
return
findUsernameInput
().
vm
.
$emit
(
'
input
'
,
username
);
};
const
username
=
'
username
'
;
const
badUsername
=
'
bad_username
'
;
const
userDeletionObstacles
=
'
["schedule1", "policy1"]
'
;
const
userDeletionObstacles
=
[
'
schedule1
'
,
'
policy1
'
];
const
mockModalData
=
{
username
,
blockPath
:
TEST_BLOCK_USER_URL
,
deletePath
:
TEST_DELETE_USER_URL
,
userDeletionObstacles
,
i18n
:
{
title
:
'
Modal for %{username}
'
,
primaryButtonLabel
:
'
Delete user
'
,
messageBody
:
'
Delete %{username} or rather %{strongStart}block user%{strongEnd}?
'
,
},
};
const
createComponent
=
(
props
=
{},
stubs
=
{})
=>
{
wrapper
=
shallowMount
(
DeleteUserModal
,
{
const
createComponent
=
(
stubs
=
{})
=>
{
wrapper
=
shallowMount
Extended
(
DeleteUserModal
,
{
propsData
:
{
username
,
title
:
'
title
'
,
content
:
'
content
'
,
action
:
'
action
'
,
secondaryAction
:
'
secondaryAction
'
,
deleteUserUrl
:
TEST_DELETE_USER_URL
,
blockUserUrl
:
TEST_BLOCK_USER_URL
,
csrfToken
:
TEST_CSRF
,
userDeletionObstacles
,
...
props
,
},
stubs
:
{
GlModal
:
ModalStub
,
...
...
@@ -68,7 +78,7 @@ describe('User Operation confirmation modal', () => {
it
(
'
renders modal with form included
'
,
()
=>
{
createComponent
();
expect
(
wrapper
.
element
).
toMatchSnapshot
();
expect
(
findForm
()
.
element
).
toMatchSnapshot
();
});
describe
(
'
on created
'
,
()
=>
{
...
...
@@ -83,11 +93,11 @@ describe('User Operation confirmation modal', () => {
});
describe
(
'
with incorrect username
'
,
()
=>
{
beforeEach
(
async
()
=>
{
beforeEach
(()
=>
{
createComponent
();
setUsername
(
badUsername
);
emitOpenModalEvent
(
mockModalData
);
await
nextTick
(
);
return
setUsername
(
badUsername
);
});
it
(
'
shows incorrect username
'
,
()
=>
{
...
...
@@ -101,11 +111,11 @@ describe('User Operation confirmation modal', () => {
});
describe
(
'
with correct username
'
,
()
=>
{
beforeEach
(
async
()
=>
{
beforeEach
(()
=>
{
createComponent
();
setUsername
(
username
);
emitOpenModalEvent
(
mockModalData
);
await
nextTick
(
);
return
setUsername
(
username
);
});
it
(
'
shows correct username
'
,
()
=>
{
...
...
@@ -117,11 +127,9 @@ describe('User Operation confirmation modal', () => {
expect
(
findSecondaryButton
().
attributes
(
'
disabled
'
)).
toBeFalsy
();
});
describe
(
'
when primary action is submitted
'
,
()
=>
{
beforeEach
(
async
()
=>
{
findPrimaryButton
().
vm
.
$emit
(
'
click
'
);
await
nextTick
();
describe
(
'
when primary action is clicked
'
,
()
=>
{
beforeEach
(()
=>
{
return
findPrimaryButton
().
vm
.
$emit
(
'
click
'
);
});
it
(
'
clears the input
'
,
()
=>
{
...
...
@@ -136,11 +144,9 @@ describe('User Operation confirmation modal', () => {
});
});
describe
(
'
when secondary action is submitted
'
,
()
=>
{
beforeEach
(
async
()
=>
{
findSecondaryButton
().
vm
.
$emit
(
'
click
'
);
await
nextTick
();
describe
(
'
when secondary action is clicked
'
,
()
=>
{
beforeEach
(()
=>
{
return
findSecondaryButton
().
vm
.
$emit
(
'
click
'
);
});
it
(
'
has correct form attributes and calls submit
'
,
()
=>
{
...
...
@@ -154,22 +160,23 @@ describe('User Operation confirmation modal', () => {
describe
(
"
when user's name has leading and trailing whitespace
"
,
()
=>
{
beforeEach
(()
=>
{
createComponent
(
{
username
:
'
John Smith
'
,
},
{
GlSprintf
},
);
createComponent
({
GlSprintf
});
return
emitOpenModalEvent
({
...
mockModalData
,
username
:
'
John Smith
'
});
});
it
(
"
displays user's name without whitespace
"
,
()
=>
{
expect
(
wrapper
.
element
).
toMatchSnapshot
();
expect
(
findMessageUsername
().
text
()).
toBe
(
'
John Smith
'
);
expect
(
findConfirmUsername
().
text
()).
toBe
(
'
John Smith
'
);
});
it
(
"
shows enabled buttons when user's name is entered without whitespace
"
,
async
()
=>
{
setUsername
(
'
John Smith
'
);
it
(
'
passes user name without whitespace to the obstacles
'
,
()
=>
{
expect
(
findUserDeletionObstaclesList
().
props
()).
toMatchObject
({
userName
:
'
John Smith
'
,
});
});
await
nextTick
();
it
(
"
shows enabled buttons when user's name is entered without whitespace
"
,
async
()
=>
{
await
setUsername
(
'
John Smith
'
);
expect
(
findPrimaryButton
().
attributes
(
'
disabled
'
)).
toBeUndefined
();
expect
(
findSecondaryButton
().
attributes
(
'
disabled
'
)).
toBeUndefined
();
...
...
@@ -177,17 +184,20 @@ describe('User Operation confirmation modal', () => {
});
describe
(
'
Related user-deletion-obstacles list
'
,
()
=>
{
it
(
'
does NOT render the list when user has no related obstacles
'
,
()
=>
{
createComponent
({
userDeletionObstacles
:
'
[]
'
});
it
(
'
does NOT render the list when user has no related obstacles
'
,
async
()
=>
{
createComponent
();
await
emitOpenModalEvent
({
...
mockModalData
,
userDeletionObstacles
:
[]
});
expect
(
findUserDeletionObstaclesList
().
exists
()).
toBe
(
false
);
});
it
(
'
renders the list when user has related obstalces
'
,
()
=>
{
it
(
'
renders the list when user has related obstalces
'
,
async
()
=>
{
createComponent
();
await
emitOpenModalEvent
(
mockModalData
);
const
obstacles
=
findUserDeletionObstaclesList
();
expect
(
obstacles
.
exists
()).
toBe
(
true
);
expect
(
obstacles
.
props
(
'
obstacles
'
)).
toEqual
(
JSON
.
parse
(
userDeletionObstacles
)
);
expect
(
obstacles
.
props
(
'
obstacles
'
)).
toEqual
(
userDeletionObstacles
);
});
});
});
spec/frontend/admin/users/components/modals/user_modal_manager_spec.js
deleted
100644 → 0
View file @
f0c53b2b
import
{
mount
}
from
'
@vue/test-utils
'
;
import
{
nextTick
}
from
'
vue
'
;
import
UserModalManager
from
'
~/admin/users/components/modals/user_modal_manager.vue
'
;
import
ModalStub
from
'
./stubs/modal_stub
'
;
describe
(
'
Users admin page Modal Manager
'
,
()
=>
{
let
wrapper
;
const
modalConfiguration
=
{
action1
:
{
title
:
'
action1
'
,
content
:
'
Action Modal 1
'
,
},
action2
:
{
title
:
'
action2
'
,
content
:
'
Action Modal 2
'
,
},
};
const
findModal
=
()
=>
wrapper
.
find
({
ref
:
'
modal
'
});
const
createComponent
=
(
props
=
{})
=>
{
wrapper
=
mount
(
UserModalManager
,
{
propsData
:
{
selector
:
'
.js-delete-user-modal-button
'
,
modalConfiguration
,
csrfToken
:
'
dummyCSRF
'
,
...
props
,
},
stubs
:
{
DeleteUserModal
:
ModalStub
,
},
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
describe
(
'
render behavior
'
,
()
=>
{
it
(
'
does not renders modal when initialized
'
,
()
=>
{
createComponent
();
expect
(
findModal
().
exists
()).
toBeFalsy
();
});
it
(
'
throws if action has no proper configuration
'
,
()
=>
{
createComponent
({
modalConfiguration
:
{},
});
expect
(()
=>
wrapper
.
vm
.
show
({
glModalAction
:
'
action1
'
})).
toThrow
();
});
it
(
'
renders modal with expected props when valid configuration is passed
'
,
async
()
=>
{
createComponent
();
wrapper
.
vm
.
show
({
glModalAction
:
'
action1
'
,
extraProp
:
'
extraPropValue
'
,
});
await
nextTick
();
const
modal
=
findModal
();
expect
(
modal
.
exists
()).
toBeTruthy
();
expect
(
modal
.
vm
.
$attrs
.
csrfToken
).
toEqual
(
'
dummyCSRF
'
);
expect
(
modal
.
vm
.
$attrs
.
extraProp
).
toEqual
(
'
extraPropValue
'
);
expect
(
modal
.
vm
.
showWasCalled
).
toBeTruthy
();
});
});
describe
(
'
click handling
'
,
()
=>
{
let
button
;
let
button2
;
const
createButtons
=
()
=>
{
button
=
document
.
createElement
(
'
button
'
);
button2
=
document
.
createElement
(
'
button
'
);
button
.
setAttribute
(
'
class
'
,
'
js-delete-user-modal-button
'
);
button
.
setAttribute
(
'
data-username
'
,
'
foo
'
);
button
.
setAttribute
(
'
data-gl-modal-action
'
,
'
action1
'
);
button
.
setAttribute
(
'
data-block-user-url
'
,
'
/block
'
);
button
.
setAttribute
(
'
data-delete-user-url
'
,
'
/delete
'
);
document
.
body
.
appendChild
(
button
);
document
.
body
.
appendChild
(
button2
);
};
const
removeButtons
=
()
=>
{
button
.
remove
();
button
=
null
;
button2
.
remove
();
button2
=
null
;
};
beforeEach
(()
=>
{
createButtons
();
createComponent
();
});
afterEach
(()
=>
{
removeButtons
();
});
it
(
'
renders the modal when the button is clicked
'
,
async
()
=>
{
button
.
click
();
await
nextTick
();
expect
(
findModal
().
exists
()).
toBe
(
true
);
});
it
(
'
does not render the modal when a misconfigured button is clicked
'
,
async
()
=>
{
button
.
removeAttribute
(
'
data-gl-modal-action
'
);
button
.
click
();
await
nextTick
();
expect
(
findModal
().
exists
()).
toBe
(
false
);
});
it
(
'
does not render the modal when a button without the selector class is clicked
'
,
async
()
=>
{
button2
.
click
();
await
nextTick
();
expect
(
findModal
().
exists
()).
toBe
(
false
);
});
});
});
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