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
afe6e9e1
Commit
afe6e9e1
authored
May 19, 2021
by
David O'Regan
Committed by
Natalia Tepluhina
May 19, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Allow issue type change for incidents
parent
57422633
Changes
34
Hide whitespace changes
Inline
Side-by-side
Showing
34 changed files
with
820 additions
and
206 deletions
+820
-206
app/assets/javascripts/issue_show/components/app.vue
app/assets/javascripts/issue_show/components/app.vue
+36
-4
app/assets/javascripts/issue_show/components/edit_actions.vue
...assets/javascripts/issue_show/components/edit_actions.vue
+95
-43
app/assets/javascripts/issue_show/components/fields/description_template.vue
...pts/issue_show/components/fields/description_template.vue
+2
-2
app/assets/javascripts/issue_show/components/fields/title.vue
...assets/javascripts/issue_show/components/fields/title.vue
+1
-1
app/assets/javascripts/issue_show/components/fields/type.vue
app/assets/javascripts/issue_show/components/fields/type.vue
+79
-0
app/assets/javascripts/issue_show/components/form.vue
app/assets/javascripts/issue_show/components/form.vue
+27
-21
app/assets/javascripts/issue_show/constants.js
app/assets/javascripts/issue_show/constants.js
+11
-0
app/assets/javascripts/issue_show/graphql.js
app/assets/javascripts/issue_show/graphql.js
+9
-0
app/assets/javascripts/issue_show/incident.js
app/assets/javascripts/issue_show/incident.js
+15
-7
app/assets/javascripts/issue_show/issue.js
app/assets/javascripts/issue_show/issue.js
+23
-8
app/assets/javascripts/issue_show/queries/get_issue_state.query.graphql
...ascripts/issue_show/queries/get_issue_state.query.graphql
+3
-0
app/assets/javascripts/issue_show/queries/update_issue_state.mutation.graphql
...ts/issue_show/queries/update_issue_state.mutation.graphql
+3
-0
app/assets/javascripts/sidebar/graphql.js
app/assets/javascripts/sidebar/graphql.js
+18
-7
app/services/issues/base_service.rb
app/services/issues/base_service.rb
+32
-0
app/services/issues/create_service.rb
app/services/issues/create_service.rb
+0
-17
app/services/issues/update_service.rb
app/services/issues/update_service.rb
+10
-0
changelogs/unreleased/268370-change-issue-type-fe-delete-button.yml
.../unreleased/268370-change-issue-type-fe-delete-button.yml
+5
-0
doc/user/project/issues/img/issue_type_change_v13_12.png
doc/user/project/issues/img/issue_type_change_v13_12.png
+0
-0
doc/user/project/issues/managing_issues.md
doc/user/project/issues/managing_issues.md
+11
-0
ee/spec/features/epics/delete_epic_spec.rb
ee/spec/features/epics/delete_epic_spec.rb
+3
-3
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/features/incidents/incident_details_spec.rb
spec/features/incidents/incident_details_spec.rb
+38
-0
spec/features/issues/issue_detail_spec.rb
spec/features/issues/issue_detail_spec.rb
+58
-5
spec/frontend/issue_show/components/app_spec.js
spec/frontend/issue_show/components/app_spec.js
+23
-18
spec/frontend/issue_show/components/description_spec.js
spec/frontend/issue_show/components/description_spec.js
+1
-1
spec/frontend/issue_show/components/edit_actions_spec.js
spec/frontend/issue_show/components/edit_actions_spec.js
+115
-65
spec/frontend/issue_show/components/fields/type_spec.js
spec/frontend/issue_show/components/fields/type_spec.js
+84
-0
spec/frontend/issue_show/components/form_spec.js
spec/frontend/issue_show/components/form_spec.js
+17
-0
spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
...end/issue_show/components/incidents/incident_tabs_spec.js
+1
-1
spec/frontend/issue_show/issue_spec.js
spec/frontend/issue_show/issue_spec.js
+1
-1
spec/frontend/issue_show/mock_data/apollo_mock.js
spec/frontend/issue_show/mock_data/apollo_mock.js
+9
-0
spec/frontend/issue_show/mock_data/mock_data.js
spec/frontend/issue_show/mock_data/mock_data.js
+1
-0
spec/services/issues/create_service_spec.rb
spec/services/issues/create_service_spec.rb
+2
-2
spec/services/issues/update_service_spec.rb
spec/services/issues/update_service_spec.rb
+84
-0
No files found.
app/assets/javascripts/issue_show/components/app.vue
View file @
afe6e9e1
...
@@ -5,8 +5,16 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
...
@@ -5,8 +5,16 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import
Poll
from
'
~/lib/utils/poll
'
;
import
Poll
from
'
~/lib/utils/poll
'
;
import
{
visitUrl
}
from
'
~/lib/utils/url_utility
'
;
import
{
visitUrl
}
from
'
~/lib/utils/url_utility
'
;
import
{
__
,
s__
,
sprintf
}
from
'
~/locale
'
;
import
{
__
,
s__
,
sprintf
}
from
'
~/locale
'
;
import
{
IssuableStatus
,
IssuableStatusText
,
IssuableType
}
from
'
../constants
'
;
import
{
IssuableStatus
,
IssuableStatusText
,
IssuableType
,
IssueTypePath
,
IncidentTypePath
,
IncidentType
,
}
from
'
../constants
'
;
import
eventHub
from
'
../event_hub
'
;
import
eventHub
from
'
../event_hub
'
;
import
getIssueStateQuery
from
'
../queries/get_issue_state.query.graphql
'
;
import
Service
from
'
../services/index
'
;
import
Service
from
'
../services/index
'
;
import
Store
from
'
../stores
'
;
import
Store
from
'
../stores
'
;
import
descriptionComponent
from
'
./description.vue
'
;
import
descriptionComponent
from
'
./description.vue
'
;
...
@@ -195,8 +203,14 @@ export default {
...
@@ -195,8 +203,14 @@ export default {
showForm
:
false
,
showForm
:
false
,
templatesRequested
:
false
,
templatesRequested
:
false
,
isStickyHeaderShowing
:
false
,
isStickyHeaderShowing
:
false
,
issueState
:
{},
};
};
},
},
apollo
:
{
issueState
:
{
query
:
getIssueStateQuery
,
},
},
computed
:
{
computed
:
{
issuableTemplates
()
{
issuableTemplates
()
{
return
this
.
store
.
formState
.
issuableTemplates
;
return
this
.
store
.
formState
.
issuableTemplates
;
...
@@ -288,7 +302,7 @@ export default {
...
@@ -288,7 +302,7 @@ export default {
methods
:
{
methods
:
{
handleBeforeUnloadEvent
(
e
)
{
handleBeforeUnloadEvent
(
e
)
{
const
event
=
e
;
const
event
=
e
;
if
(
this
.
showForm
&&
this
.
issueChanged
)
{
if
(
this
.
showForm
&&
this
.
issueChanged
&&
!
this
.
issueState
.
isDirty
)
{
event
.
returnValue
=
__
(
'
Are you sure you want to lose your issue information?
'
);
event
.
returnValue
=
__
(
'
Are you sure you want to lose your issue information?
'
);
}
}
return
undefined
;
return
undefined
;
...
@@ -346,14 +360,32 @@ export default {
...
@@ -346,14 +360,32 @@ export default {
},
},
updateIssuable
()
{
updateIssuable
()
{
const
{
store
:
{
formState
},
issueState
,
}
=
this
;
const
issuablePayload
=
issueState
.
isDirty
?
{
...
formState
,
issue_type
:
issueState
.
issueType
}
:
formState
;
this
.
clearFlash
();
this
.
clearFlash
();
return
this
.
service
return
this
.
service
.
updateIssuable
(
this
.
store
.
formState
)
.
updateIssuable
(
issuablePayload
)
.
then
((
res
)
=>
res
.
data
)
.
then
((
res
)
=>
res
.
data
)
.
then
((
data
)
=>
{
.
then
((
data
)
=>
{
if
(
!
window
.
location
.
pathname
.
includes
(
data
.
web_url
))
{
if
(
!
window
.
location
.
pathname
.
includes
(
data
.
web_url
)
&&
issueState
.
issueType
!==
IncidentType
)
{
visitUrl
(
data
.
web_url
);
visitUrl
(
data
.
web_url
);
}
}
if
(
issueState
.
isDirty
)
{
const
URI
=
issueState
.
issueType
===
IncidentType
?
data
.
web_url
.
replace
(
IssueTypePath
,
IncidentTypePath
)
:
data
.
web_url
;
visitUrl
(
URI
);
}
})
})
.
then
(
this
.
updateStoreState
)
.
then
(
this
.
updateStoreState
)
.
then
(()
=>
{
.
then
(()
=>
{
...
...
app/assets/javascripts/issue_show/components/edit_actions.vue
View file @
afe6e9e1
<
script
>
<
script
>
import
{
GlButton
}
from
'
@gitlab/ui
'
;
import
{
GlButton
,
GlModal
,
GlModalDirective
}
from
'
@gitlab/ui
'
;
import
{
uniqueId
}
from
'
lodash
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
eventHub
from
'
../event_hub
'
;
import
eventHub
from
'
../event_hub
'
;
import
updateMixin
from
'
../mixins/update
'
;
import
updateMixin
from
'
../mixins/update
'
;
import
getIssueStateQuery
from
'
../queries/get_issue_state.query.graphql
'
;
const
issuableTypes
=
{
const
issuableTypes
=
{
issue
:
__
(
'
Issue
'
),
issue
:
__
(
'
Issue
'
),
epic
:
__
(
'
Epic
'
),
epic
:
__
(
'
Epic
'
),
incident
:
__
(
'
Incident
'
),
};
};
export
default
{
export
default
{
components
:
{
components
:
{
GlButton
,
GlButton
,
GlModal
,
},
directives
:
{
GlModal
:
GlModalDirective
,
},
},
mixins
:
[
updateMixin
],
mixins
:
[
updateMixin
],
props
:
{
props
:
{
...
@@ -36,19 +43,56 @@ export default {
...
@@ -36,19 +43,56 @@ export default {
data
()
{
data
()
{
return
{
return
{
deleteLoading
:
false
,
deleteLoading
:
false
,
skipApollo
:
false
,
issueState
:
{},
modalId
:
uniqueId
(
'
delete-issuable-modal-
'
),
};
};
},
},
apollo
:
{
issueState
:
{
query
:
getIssueStateQuery
,
skip
()
{
return
this
.
skipApollo
;
},
result
()
{
this
.
skipApollo
=
true
;
},
},
},
computed
:
{
computed
:
{
deleteIssuableButtonText
()
{
return
sprintf
(
__
(
'
Delete %{issuableType}
'
),
{
issuableType
:
this
.
typeToShow
.
toLowerCase
(),
});
},
deleteIssuableModalText
()
{
return
this
.
issuableType
===
'
epic
'
?
__
(
'
Delete this epic and all descendants?
'
)
:
sprintf
(
__
(
'
%{issuableType} will be removed! Are you sure?
'
),
{
issuableType
:
this
.
typeToShow
,
});
},
isSubmitEnabled
()
{
isSubmitEnabled
()
{
return
this
.
formState
.
title
.
trim
()
!==
''
;
return
this
.
formState
.
title
.
trim
()
!==
''
;
},
},
modalActionProps
()
{
return
{
primary
:
{
text
:
this
.
deleteIssuableButtonText
,
attributes
:
[{
variant
:
'
danger
'
},
{
loading
:
this
.
deleteLoading
}],
},
cancel
:
{
text
:
__
(
'
Cancel
'
),
},
};
},
shouldShowDeleteButton
()
{
shouldShowDeleteButton
()
{
return
this
.
canDestroy
&&
this
.
showDeleteButton
;
return
this
.
canDestroy
&&
this
.
showDeleteButton
;
},
},
deleteIssuableButtonText
()
{
typeToShow
()
{
return
sprintf
(
__
(
'
Delete %{issuableType}
'
),
{
const
{
issueState
,
issuableType
}
=
this
;
issuableType
:
issuableTypes
[
this
.
issuableType
].
toLowerCase
(),
const
type
=
issueState
.
issueType
??
issuableType
;
})
;
return
issuableTypes
[
type
]
;
},
},
},
},
methods
:
{
methods
:
{
...
@@ -56,49 +100,57 @@ export default {
...
@@ -56,49 +100,57 @@ export default {
eventHub
.
$emit
(
'
close.form
'
);
eventHub
.
$emit
(
'
close.form
'
);
},
},
deleteIssuable
()
{
deleteIssuable
()
{
const
confirmMessage
=
this
.
deleteLoading
=
true
;
this
.
issuableType
===
'
epic
'
eventHub
.
$emit
(
'
delete.issuable
'
,
{
destroy_confirm
:
true
});
?
__
(
'
Delete this epic and all descendants?
'
)
:
sprintf
(
__
(
'
%{issuableType} will be removed! Are you sure?
'
),
{
issuableType
:
issuableTypes
[
this
.
issuableType
],
});
// eslint-disable-next-line no-alert
if
(
window
.
confirm
(
confirmMessage
))
{
this
.
deleteLoading
=
true
;
eventHub
.
$emit
(
'
delete.issuable
'
,
{
destroy_confirm
:
true
});
}
},
},
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<div
class=
"gl-mt-3 gl-mb-3 clearfix"
>
<div
class=
"gl-mt-3 gl-mb-3 gl-display-flex gl-justify-content-space-between"
>
<gl-button
<div>
:loading=
"formState.updateLoading"
<gl-button
:disabled=
"formState.updateLoading || !isSubmitEnabled"
:loading=
"formState.updateLoading"
category=
"primary"
:disabled=
"formState.updateLoading || !isSubmitEnabled"
variant=
"confirm"
category=
"primary"
class=
"float-left qa-save-button gl-mr-3"
variant=
"confirm"
type=
"submit"
class=
"qa-save-button gl-mr-3"
@
click.prevent=
"updateIssuable"
data-testid=
"issuable-save-button"
>
type=
"submit"
{{
__
(
'
Save changes
'
)
}}
@
click.prevent=
"updateIssuable"
</gl-button>
>
<gl-button
@
click=
"closeForm"
>
{{
__
(
'
Save changes
'
)
}}
{{
__
(
'
Cancel
'
)
}}
</gl-button>
</gl-button>
<gl-button
data-testid=
"issuable-cancel-button"
@
click=
"closeForm"
>
<gl-button
{{
__
(
'
Cancel
'
)
}}
v-if=
"shouldShowDeleteButton"
</gl-button>
:loading=
"deleteLoading"
</div>
:disabled=
"deleteLoading"
<div
v-if=
"shouldShowDeleteButton"
>
category=
"secondary"
<gl-button
variant=
"danger"
v-gl-modal=
"modalId"
class=
"float-right qa-delete-button"
:loading=
"deleteLoading"
@
click=
"deleteIssuable"
:disabled=
"deleteLoading"
>
category=
"secondary"
{{
deleteIssuableButtonText
}}
variant=
"danger"
</gl-button>
class=
"qa-delete-button"
data-testid=
"issuable-delete-button"
>
{{
deleteIssuableButtonText
}}
</gl-button>
<gl-modal
ref=
"removeModal"
:modal-id=
"modalId"
size=
"sm"
:action-primary=
"modalActionProps.primary"
:action-cancel=
"modalActionProps.cancel"
@
primary=
"deleteIssuable"
>
<template
#modal-title
>
{{
deleteIssuableButtonText
}}
</
template
>
<div>
<p
class=
"gl-mb-1"
>
{{ deleteIssuableModalText }}
</p>
</div>
</gl-modal>
</div>
</div>
</div>
</template>
</template>
app/assets/javascripts/issue_show/components/fields/description_template.vue
View file @
afe6e9e1
...
@@ -54,14 +54,14 @@ export default {
...
@@ -54,14 +54,14 @@ export default {
<
template
>
<
template
>
<!-- eslint-disable @gitlab/vue-no-data-toggle -->
<!-- eslint-disable @gitlab/vue-no-data-toggle -->
<div
class=
"dropdown js-issuable-selector-wrap"
data-issuable-type=
"issues"
>
<div
class=
"dropdown js-issuable-selector-wrap
gl-mb-0
"
data-issuable-type=
"issues"
>
<button
<button
ref=
"toggle"
ref=
"toggle"
:data-namespace-path=
"projectNamespace"
:data-namespace-path=
"projectNamespace"
:data-project-path=
"projectPath"
:data-project-path=
"projectPath"
:data-project-id=
"projectId"
:data-project-id=
"projectId"
:data-data=
"issuableTemplatesJson"
:data-data=
"issuableTemplatesJson"
class=
"dropdown-menu-toggle js-issuable-selector"
class=
"dropdown-menu-toggle js-issuable-selector
gl-button
"
type=
"button"
type=
"button"
data-field-name=
"issuable_template"
data-field-name=
"issuable_template"
data-selected=
"null"
data-selected=
"null"
...
...
app/assets/javascripts/issue_show/components/fields/title.vue
View file @
afe6e9e1
...
@@ -20,7 +20,7 @@ export default {
...
@@ -20,7 +20,7 @@ export default {
id=
"issuable-title"
id=
"issuable-title"
ref=
"input"
ref=
"input"
v-model=
"formState.title"
v-model=
"formState.title"
class=
"form-control qa-title-input"
class=
"form-control qa-title-input
gl-border-gray-200
"
dir=
"auto"
dir=
"auto"
type=
"text"
type=
"text"
:placeholder=
"__('Title')"
:placeholder=
"__('Title')"
...
...
app/assets/javascripts/issue_show/components/fields/type.vue
0 → 100644
View file @
afe6e9e1
<
script
>
import
{
GlFormGroup
,
GlDropdown
,
GlDropdownItem
}
from
'
@gitlab/ui
'
;
import
{
capitalize
}
from
'
lodash
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
IssuableTypes
}
from
'
../../constants
'
;
import
getIssueStateQuery
from
'
../../queries/get_issue_state.query.graphql
'
;
import
updateIssueStateMutation
from
'
../../queries/update_issue_state.mutation.graphql
'
;
export
const
i18n
=
{
label
:
__
(
'
Issue Type
'
),
};
export
default
{
i18n
,
IssuableTypes
,
components
:
{
GlFormGroup
,
GlDropdown
,
GlDropdownItem
,
},
data
()
{
return
{
issueState
:
{},
};
},
apollo
:
{
issueState
:
{
query
:
getIssueStateQuery
,
},
},
computed
:
{
dropdownText
()
{
const
{
issueState
:
{
issueType
},
}
=
this
;
return
capitalize
(
issueType
);
},
},
methods
:
{
updateIssueType
(
issueType
)
{
this
.
$apollo
.
mutate
({
mutation
:
updateIssueStateMutation
,
variables
:
{
issueType
,
isDirty
:
true
,
},
});
},
},
};
</
script
>
<
template
>
<gl-form-group
:label=
"$options.i18n.label"
label-class=
"sr-only"
label-for=
"issuable-type"
class=
"mb-2 mb-md-0"
>
<gl-dropdown
id=
"issuable-type"
:aria-labelledby=
"$options.i18n.label"
:text=
"dropdownText"
:header-text=
"$options.i18n.label"
class=
"gl-w-full"
toggle-class=
"dropdown-menu-toggle"
>
<gl-dropdown-item
v-for=
"type in $options.IssuableTypes"
:key=
"type.value"
:is-checked=
"issueState.issueType === type.value"
is-check-item
@
click=
"updateIssueType(type.value)"
>
{{
type
.
text
}}
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
</
template
>
app/assets/javascripts/issue_show/components/form.vue
View file @
afe6e9e1
...
@@ -2,21 +2,24 @@
...
@@ -2,21 +2,24 @@
import
{
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
GlAlert
}
from
'
@gitlab/ui
'
;
import
$
from
'
jquery
'
;
import
$
from
'
jquery
'
;
import
Autosave
from
'
~/autosave
'
;
import
Autosave
from
'
~/autosave
'
;
import
{
IssuableType
}
from
'
~/issue_show/constants
'
;
import
eventHub
from
'
../event_hub
'
;
import
eventHub
from
'
../event_hub
'
;
import
editActions
from
'
./edit_actions.vue
'
;
import
EditActions
from
'
./edit_actions.vue
'
;
import
descriptionField
from
'
./fields/description.vue
'
;
import
DescriptionField
from
'
./fields/description.vue
'
;
import
descriptionTemplate
from
'
./fields/description_template.vue
'
;
import
DescriptionTemplateField
from
'
./fields/description_template.vue
'
;
import
titleField
from
'
./fields/title.vue
'
;
import
IssuableTitleField
from
'
./fields/title.vue
'
;
import
lockedWarning
from
'
./locked_warning.vue
'
;
import
IssuableTypeField
from
'
./fields/type.vue
'
;
import
LockedWarning
from
'
./locked_warning.vue
'
;
export
default
{
export
default
{
components
:
{
components
:
{
lockedWarning
,
DescriptionField
,
titleField
,
DescriptionTemplateField
,
descriptionField
,
EditActions
,
descriptionTemplate
,
editActions
,
GlAlert
,
GlAlert
,
IssuableTitleField
,
IssuableTypeField
,
LockedWarning
,
},
},
props
:
{
props
:
{
canDestroy
:
{
canDestroy
:
{
...
@@ -89,6 +92,9 @@ export default {
...
@@ -89,6 +92,9 @@ export default {
showLockedWarning
()
{
showLockedWarning
()
{
return
this
.
formState
.
lockedWarningVisible
&&
!
this
.
formState
.
updateLoading
;
return
this
.
formState
.
lockedWarningVisible
&&
!
this
.
formState
.
updateLoading
;
},
},
isIssueType
()
{
return
this
.
issuableType
===
IssuableType
.
Issue
;
},
},
},
created
()
{
created
()
{
eventHub
.
$on
(
'
delete.issuable
'
,
this
.
resetAutosave
);
eventHub
.
$on
(
'
delete.issuable
'
,
this
.
resetAutosave
);
...
@@ -162,7 +168,7 @@ export default {
...
@@ -162,7 +168,7 @@ export default {
</
script
>
</
script
>
<
template
>
<
template
>
<form>
<form
data-testid=
"issuable-form"
>
<locked-warning
v-if=
"showLockedWarning"
/>
<locked-warning
v-if=
"showLockedWarning"
/>
<gl-alert
<gl-alert
v-if=
"showOutdatedDescriptionWarning"
v-if=
"showOutdatedDescriptionWarning"
...
@@ -179,9 +185,17 @@ export default {
...
@@ -179,9 +185,17 @@ export default {
)
)
}}
</gl-alert
}}
</gl-alert
>
>
<div
class=
"row gl-mb-3"
>
<div
class=
"col-12"
>
<issuable-title-field
ref=
"title"
:form-state=
"formState"
/>
</div>
</div>
<div
class=
"row"
>
<div
class=
"row"
>
<div
v-if=
"hasIssuableTemplates"
class=
"col-sm-4 col-lg-3"
>
<div
v-if=
"isIssueType"
class=
"col-12 col-md-4 pr-md-0"
>
<description-template
<issuable-type-field
ref=
"issue-type"
/>
</div>
<div
v-if=
"hasIssuableTemplates"
class=
"col-12 col-md-4 pl-md-2"
>
<description-template-field
:form-state=
"formState"
:form-state=
"formState"
:issuable-templates=
"issuableTemplates"
:issuable-templates=
"issuableTemplates"
:project-path=
"projectPath"
:project-path=
"projectPath"
...
@@ -189,14 +203,6 @@ export default {
...
@@ -189,14 +203,6 @@ export default {
:project-namespace=
"projectNamespace"
:project-namespace=
"projectNamespace"
/>
/>
</div>
</div>
<div
:class=
"
{
'col-sm-8 col-lg-9': hasIssuableTemplates,
'col-12': !hasIssuableTemplates,
}"
>
<title-field
ref=
"title"
:form-state=
"formState"
:issuable-templates=
"issuableTemplates"
/>
</div>
</div>
</div>
<description-field
<description-field
ref=
"description"
ref=
"description"
...
...
app/assets/javascripts/issue_show/constants.js
View file @
afe6e9e1
...
@@ -25,3 +25,14 @@ export const IssueStateEvent = {
...
@@ -25,3 +25,14 @@ export const IssueStateEvent = {
export
const
STATUS_PAGE_PUBLISHED
=
__
(
'
Published on status page
'
);
export
const
STATUS_PAGE_PUBLISHED
=
__
(
'
Published on status page
'
);
export
const
JOIN_ZOOM_MEETING
=
__
(
'
Join Zoom meeting
'
);
export
const
JOIN_ZOOM_MEETING
=
__
(
'
Join Zoom meeting
'
);
export
const
IssuableTypes
=
[
{
value
:
'
issue
'
,
text
:
__
(
'
Issue
'
)
},
{
value
:
'
incident
'
,
text
:
__
(
'
Incident
'
)
},
];
export
const
IssueTypePath
=
'
issues
'
;
export
const
IncidentTypePath
=
'
issues/incident
'
;
export
const
IncidentType
=
'
incident
'
;
export
const
issueState
=
{
issueType
:
undefined
,
isDirty
:
false
};
app/assets/javascripts/issue_show/graphql.js
0 → 100644
View file @
afe6e9e1
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
{
defaultClient
}
from
'
~/sidebar/graphql
'
;
Vue
.
use
(
VueApollo
);
export
default
new
VueApollo
({
defaultClient
,
});
app/assets/javascripts/issue_show/incident.js
View file @
afe6e9e1
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createDefaultClient
from
'
~/lib/graphql
'
;
import
{
parseBoolean
}
from
'
~/lib/utils/common_utils
'
;
import
{
parseBoolean
}
from
'
~/lib/utils/common_utils
'
;
import
issuableApp
from
'
./components/app.vue
'
;
import
issuableApp
from
'
./components/app.vue
'
;
import
incidentTabs
from
'
./components/incidents/incident_tabs.vue
'
;
import
incidentTabs
from
'
./components/incidents/incident_tabs.vue
'
;
import
{
issueState
}
from
'
./constants
'
;
Vue
.
use
(
VueApollo
);
import
apolloProvider
from
'
./graphql
'
;
import
getIssueStateQuery
from
'
./queries/get_issue_state.query.graphql
'
;
export
default
function
initIssuableApp
(
issuableData
=
{})
{
export
default
function
initIssuableApp
(
issuableData
=
{})
{
const
apolloProvider
=
new
VueApollo
({
const
el
=
document
.
getElementById
(
'
js-issuable-app
'
);
defaultClient
:
createDefaultClient
(),
if
(
!
el
)
{
return
undefined
;
}
apolloProvider
.
clients
.
defaultClient
.
cache
.
writeQuery
({
query
:
getIssueStateQuery
,
data
:
{
issueState
:
{
...
issueState
,
issueType
:
el
.
dataset
.
issueType
},
},
});
});
const
{
const
{
...
@@ -25,7 +33,7 @@ export default function initIssuableApp(issuableData = {}) {
...
@@ -25,7 +33,7 @@ export default function initIssuableApp(issuableData = {}) {
const
fullPath
=
`
${
projectNamespace
}
/
${
projectPath
}
`
;
const
fullPath
=
`
${
projectNamespace
}
/
${
projectPath
}
`
;
return
new
Vue
({
return
new
Vue
({
el
:
document
.
getElementById
(
'
js-issuable-app
'
)
,
el
,
apolloProvider
,
apolloProvider
,
components
:
{
components
:
{
issuableApp
,
issuableApp
,
...
...
app/assets/javascripts/issue_show/issue.js
View file @
afe6e9e1
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
{
mapGetters
}
from
'
vuex
'
;
import
{
mapGetters
}
from
'
vuex
'
;
import
createDefaultClient
from
'
~/lib/graphql
'
;
import
{
parseBoolean
}
from
'
~/lib/utils/common_utils
'
;
import
{
parseBoolean
}
from
'
~/lib/utils/common_utils
'
;
import
IssuableApp
from
'
./components/app.vue
'
;
import
IssuableApp
from
'
./components/app.vue
'
;
import
HeaderActions
from
'
./components/header_actions.vue
'
;
import
HeaderActions
from
'
./components/header_actions.vue
'
;
import
{
issueState
}
from
'
./constants
'
;
import
apolloProvider
from
'
./graphql
'
;
import
getIssueStateQuery
from
'
./queries/get_issue_state.query.graphql
'
;
const
bootstrapApollo
=
(
state
=
{})
=>
{
return
apolloProvider
.
clients
.
defaultClient
.
cache
.
writeQuery
({
query
:
getIssueStateQuery
,
data
:
{
issueState
:
state
,
},
});
};
export
function
initIssuableApp
(
issuableData
,
store
)
{
export
function
initIssuableApp
(
issuableData
,
store
)
{
const
el
=
document
.
getElementById
(
'
js-issuable-app
'
);
if
(
!
el
)
{
return
undefined
;
}
bootstrapApollo
({
...
issueState
,
issueType
:
el
.
dataset
.
issueType
});
return
new
Vue
({
return
new
Vue
({
el
:
document
.
getElementById
(
'
js-issuable-app
'
),
el
,
apolloProvider
,
store
,
store
,
computed
:
{
computed
:
{
...
mapGetters
([
'
getNoteableData
'
]),
...
mapGetters
([
'
getNoteableData
'
]),
...
@@ -33,11 +52,7 @@ export function initIssueHeaderActions(store) {
...
@@ -33,11 +52,7 @@ export function initIssueHeaderActions(store) {
return
undefined
;
return
undefined
;
}
}
Vue
.
use
(
VueApollo
);
bootstrapApollo
({
...
issueState
,
issueType
:
el
.
dataset
.
issueType
});
const
apolloProvider
=
new
VueApollo
({
defaultClient
:
createDefaultClient
(),
});
return
new
Vue
({
return
new
Vue
({
el
,
el
,
...
...
app/assets/javascripts/issue_show/queries/get_issue_state.query.graphql
0 → 100644
View file @
afe6e9e1
query
issueState
{
issueState
@client
}
app/assets/javascripts/issue_show/queries/update_issue_state.mutation.graphql
0 → 100644
View file @
afe6e9e1
mutation
updateIssueState
(
$issueType
:
String
,
$isDirty
:
Boolean
)
{
updateIssueState
(
issueType
:
$issueType
,
isDirty
:
$isDirty
)
@client
}
app/assets/javascripts/sidebar/graphql.js
View file @
afe6e9e1
import
{
IntrospectionFragmentMatcher
}
from
'
apollo-cache-inmemory
'
;
import
{
IntrospectionFragmentMatcher
}
from
'
apollo-cache-inmemory
'
;
import
produce
from
'
immer
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
getIssueStateQuery
from
'
~/issue_show/queries/get_issue_state.query.graphql
'
;
import
createDefaultClient
from
'
~/lib/graphql
'
;
import
createDefaultClient
from
'
~/lib/graphql
'
;
import
introspectionQueryResultData
from
'
./fragmentTypes.json
'
;
import
introspectionQueryResultData
from
'
./fragmentTypes.json
'
;
...
@@ -7,15 +9,24 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
...
@@ -7,15 +9,24 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData
,
introspectionQueryResultData
,
});
});
export
const
defaultClient
=
createDefaultClient
(
const
resolvers
=
{
{},
Mutation
:
{
{
updateIssueState
:
(
_
,
{
issueType
=
undefined
,
isDirty
=
false
},
{
cache
})
=>
{
cacheConfig
:
{
const
sourceData
=
cache
.
readQuery
({
query
:
getIssueStateQuery
});
fragmentMatcher
,
const
data
=
produce
(
sourceData
,
(
draftData
)
=>
{
draftData
.
issueState
=
{
issueType
,
isDirty
};
});
cache
.
writeQuery
({
query
:
getIssueStateQuery
,
data
});
},
},
assumeImmutableResults
:
true
,
},
},
);
};
export
const
defaultClient
=
createDefaultClient
(
resolvers
,
{
cacheConfig
:
{
fragmentMatcher
,
},
assumeImmutableResults
:
true
,
});
export
const
apolloProvider
=
new
VueApollo
({
export
const
apolloProvider
=
new
VueApollo
({
defaultClient
,
defaultClient
,
...
...
app/services/issues/base_service.rb
View file @
afe6e9e1
...
@@ -38,6 +38,7 @@ module Issues
...
@@ -38,6 +38,7 @@ module Issues
super
super
params
.
delete
(
:issue_type
)
unless
issue_type_allowed?
(
issue
)
params
.
delete
(
:issue_type
)
unless
issue_type_allowed?
(
issue
)
filter_incident_label
(
issue
)
if
params
[
:issue_type
]
moved_issue
=
params
.
delete
(
:moved_issue
)
moved_issue
=
params
.
delete
(
:moved_issue
)
...
@@ -82,6 +83,37 @@ module Issues
...
@@ -82,6 +83,37 @@ module Issues
def
issue_type_allowed?
(
object
)
def
issue_type_allowed?
(
object
)
can?
(
current_user
,
:"create_
#{
params
[
:issue_type
]
}
"
,
object
)
can?
(
current_user
,
:"create_
#{
params
[
:issue_type
]
}
"
,
object
)
end
end
# @param issue [Issue]
def
filter_incident_label
(
issue
)
return
unless
add_incident_label?
(
issue
)
||
remove_incident_label?
(
issue
)
label
=
::
IncidentManagement
::
CreateIncidentLabelService
.
new
(
project
,
current_user
)
.
execute
.
payload
[
:label
]
# These(add_label_ids, remove_label_ids) are being added ahead of time
# to be consumed by #process_label_ids, this allows system notes
# to be applied correctly alongside the label updates.
if
add_incident_label?
(
issue
)
params
[
:add_label_ids
]
||=
[]
params
[
:add_label_ids
]
<<
label
.
id
else
params
[
:remove_label_ids
]
||=
[]
params
[
:remove_label_ids
]
<<
label
.
id
end
end
# @param issue [Issue]
def
add_incident_label?
(
issue
)
issue
.
incident?
end
# @param _issue [Issue, nil]
def
remove_incident_label?
(
_issue
)
false
end
end
end
end
end
...
...
app/services/issues/create_service.rb
View file @
afe6e9e1
...
@@ -34,7 +34,6 @@ module Issues
...
@@ -34,7 +34,6 @@ module Issues
# Add new items to Issues::AfterCreateService if they can be performed in Sidekiq
# Add new items to Issues::AfterCreateService if they can be performed in Sidekiq
def
after_create
(
issue
)
def
after_create
(
issue
)
add_incident_label
(
issue
)
user_agent_detail_service
.
create
user_agent_detail_service
.
create
resolve_discussions_with_issue
(
issue
)
resolve_discussions_with_issue
(
issue
)
...
@@ -56,22 +55,6 @@ module Issues
...
@@ -56,22 +55,6 @@ module Issues
def
user_agent_detail_service
def
user_agent_detail_service
UserAgentDetailService
.
new
(
@issue
,
request
)
UserAgentDetailService
.
new
(
@issue
,
request
)
end
end
# Applies label "incident" (creates it if missing) to incident issues.
# For use in "after" hooks only to ensure we are not appyling
# labels prematurely.
def
add_incident_label
(
issue
)
return
unless
issue
.
incident?
label
=
::
IncidentManagement
::
CreateIncidentLabelService
.
new
(
project
,
current_user
)
.
execute
.
payload
[
:label
]
return
if
issue
.
label_ids
.
include?
(
label
.
id
)
issue
.
labels
<<
label
end
end
end
end
end
...
...
app/services/issues/update_service.rb
View file @
afe6e9e1
...
@@ -204,6 +204,16 @@ module Issues
...
@@ -204,6 +204,16 @@ module Issues
def
create_confidentiality_note
(
issue
)
def
create_confidentiality_note
(
issue
)
SystemNoteService
.
change_issue_confidentiality
(
issue
,
issue
.
project
,
current_user
)
SystemNoteService
.
change_issue_confidentiality
(
issue
,
issue
.
project
,
current_user
)
end
end
override
:add_incident_label?
def
add_incident_label?
(
issue
)
issue
.
issue_type
!=
params
[
:issue_type
]
&&
!
issue
.
incident?
end
override
:remove_incident_label?
def
remove_incident_label?
(
issue
)
issue
.
issue_type
!=
params
[
:issue_type
]
&&
issue
.
incident?
end
end
end
end
end
...
...
changelogs/unreleased/268370-change-issue-type-fe-delete-button.yml
0 → 100644
View file @
afe6e9e1
---
title
:
Allow issue type change for incidents
merge_request
:
61363
author
:
type
:
changed
doc/user/project/issues/img/issue_type_change_v13_12.png
0 → 100644
View file @
afe6e9e1
51.2 KB
doc/user/project/issues/managing_issues.md
View file @
afe6e9e1
...
@@ -326,6 +326,17 @@ In order to change the default issue closing pattern, GitLab administrators must
...
@@ -326,6 +326,17 @@ In order to change the default issue closing pattern, GitLab administrators must
[
`gitlab.rb` or `gitlab.yml` file
](
../../../administration/issue_closing_pattern.md
)
[
`gitlab.rb` or `gitlab.yml` file
](
../../../administration/issue_closing_pattern.md
)
of your installation.
of your installation.
## Change the issue type
Users with
[
developer permission
](
../../permissions.md
)
can change an issue's type. To do this, edit the issue and select an issue type from the
**Issue type**
selector menu:
-
[
Issue
](
index.md
)
-
[
Incident
](
../../../operations/incident_management/index.md
)
![
Change the issue type
](
img/issue_type_change_v13_12.png
)
## Deleting issues
## Deleting issues
Users with
[
project owner permission
](
../../permissions.md
)
can delete an issue by
Users with
[
project owner permission
](
../../permissions.md
)
can delete an issue by
...
...
ee/spec/features/epics/delete_epic_spec.rb
View file @
afe6e9e1
...
@@ -31,10 +31,10 @@ RSpec.describe 'Delete Epic', :js do
...
@@ -31,10 +31,10 @@ RSpec.describe 'Delete Epic', :js do
end
end
it
'deletes the issue and redirect to epic list'
do
it
'deletes the issue and redirect to epic list'
do
page
.
accept_alert
'Delete this epic and all descendants?'
do
find
(
'.qa-delete-button'
).
click
find
(
:button
,
text:
'Delete epic'
).
click
wait_for_requests
end
find
(
'.js-modal-action-primary'
).
click
wait_for_requests
wait_for_requests
expect
(
find
(
'.issuable-list'
)).
not_to
have_content
(
epic
.
title
)
expect
(
find
(
'.issuable-list'
)).
not_to
have_content
(
epic
.
title
)
...
...
locale/gitlab.pot
View file @
afe6e9e1
...
@@ -18230,6 +18230,9 @@ msgstr ""
...
@@ -18230,6 +18230,9 @@ msgstr ""
msgid "Issue Boards"
msgid "Issue Boards"
msgstr ""
msgstr ""
msgid "Issue Type"
msgstr ""
msgid "Issue already promoted to epic."
msgid "Issue already promoted to epic."
msgstr ""
msgstr ""
...
...
spec/features/incidents/incident_details_spec.rb
View file @
afe6e9e1
...
@@ -49,4 +49,42 @@ RSpec.describe 'Incident details', :js do
...
@@ -49,4 +49,42 @@ RSpec.describe 'Incident details', :js do
end
end
end
end
end
end
context
'when an incident `issue_type` is edited by a signed in user'
do
it
'routes the user to the incident details page when the `issue_type` is set to incident'
do
wait_for_requests
project_path
=
"/
#{
project
.
full_path
}
"
click_button
'Edit title and description'
wait_for_requests
page
.
within
(
'[data-testid="issuable-form"]'
)
do
click_button
'Incident'
click_button
'Issue'
click_button
'Save changes'
wait_for_requests
expect
(
page
).
to
have_current_path
(
"
#{
project_path
}
/-/issues/
#{
incident
.
iid
}
"
)
end
end
end
context
'when incident details are edited by a signed in user'
do
it
'routes the user to the incident details page when the `issue_type` is set to incident'
do
wait_for_requests
project_path
=
"/
#{
project
.
full_path
}
"
click_button
'Edit title and description'
wait_for_requests
page
.
within
(
'[data-testid="issuable-form"]'
)
do
click_button
'Incident'
click_button
'Issue'
click_button
'Save changes'
wait_for_requests
expect
(
page
).
to
have_current_path
(
"
#{
project_path
}
/-/issues/
#{
incident
.
iid
}
"
)
end
end
end
end
end
spec/features/issues/issue_detail_spec.rb
View file @
afe6e9e1
...
@@ -6,6 +6,7 @@ RSpec.describe 'Issue Detail', :js do
...
@@ -6,6 +6,7 @@ RSpec.describe 'Issue Detail', :js do
let
(
:user
)
{
create
(
:user
)
}
let
(
:user
)
{
create
(
:user
)
}
let
(
:project
)
{
create
(
:project
,
:public
)
}
let
(
:project
)
{
create
(
:project
,
:public
)
}
let
(
:issue
)
{
create
(
:issue
,
project:
project
,
author:
user
)
}
let
(
:issue
)
{
create
(
:issue
,
project:
project
,
author:
user
)
}
let
(
:incident
)
{
create
(
:incident
,
project:
project
,
author:
user
)
}
context
'when user displays the issue'
do
context
'when user displays the issue'
do
before
do
before
do
...
@@ -21,10 +22,8 @@ RSpec.describe 'Issue Detail', :js do
...
@@ -21,10 +22,8 @@ RSpec.describe 'Issue Detail', :js do
end
end
context
'when user displays the issue as an incident'
do
context
'when user displays the issue as an incident'
do
let
(
:issue
)
{
create
(
:incident
,
project:
project
,
author:
user
)
}
before
do
before
do
visit
project_issue_path
(
project
,
i
ssue
)
visit
project_issue_path
(
project
,
i
ncident
)
wait_for_requests
wait_for_requests
end
end
...
@@ -58,9 +57,9 @@ RSpec.describe 'Issue Detail', :js do
...
@@ -58,9 +57,9 @@ RSpec.describe 'Issue Detail', :js do
visit
project_issue_path
(
project
,
issue
)
visit
project_issue_path
(
project
,
issue
)
wait_for_requests
wait_for_requests
page
.
find
(
'.js-issuable-edit'
).
click
click_button
'Edit title and description'
fill_in
'issuable-title'
,
with:
'issue title'
fill_in
'issuable-title'
,
with:
'issue title'
click_button
'Save'
click_button
'Save
changes
'
wait_for_requests
wait_for_requests
Users
::
DestroyService
.
new
(
user
).
execute
(
user
)
Users
::
DestroyService
.
new
(
user
).
execute
(
user
)
...
@@ -74,4 +73,58 @@ RSpec.describe 'Issue Detail', :js do
...
@@ -74,4 +73,58 @@ RSpec.describe 'Issue Detail', :js do
end
end
end
end
end
end
describe
'user updates `issue_type` via the issue type dropdown'
do
context
'when an issue `issue_type` is edited by a signed in user'
do
before
do
sign_in
(
user
)
visit
project_issue_path
(
project
,
issue
)
wait_for_requests
end
it
'routes the user to the incident details page when the `issue_type` is set to incident'
do
open_issue_edit_form
page
.
within
(
'[data-testid="issuable-form"]'
)
do
update_type_select
(
'Issue'
,
'Incident'
)
expect
(
page
).
to
have_current_path
(
project_issues_incident_path
(
project
,
issue
))
end
end
end
context
'when an incident `issue_type` is edited by a signed in user'
do
before
do
sign_in
(
user
)
visit
project_issue_path
(
project
,
incident
)
wait_for_requests
end
it
'routes the user to the issue details page when the `issue_type` is set to issue'
do
open_issue_edit_form
page
.
within
(
'[data-testid="issuable-form"]'
)
do
update_type_select
(
'Incident'
,
'Issue'
)
expect
(
page
).
to
have_current_path
(
project_issue_path
(
project
,
incident
))
end
end
end
end
def
update_type_select
(
from
,
to
)
click_button
from
click_button
to
click_button
'Save changes'
wait_for_requests
end
def
open_issue_edit_form
wait_for_requests
click_button
'Edit title and description'
wait_for_requests
end
end
end
spec/frontend/issue_show/components/app_spec.js
View file @
afe6e9e1
import
{
GlIntersectionObserver
}
from
'
@gitlab/ui
'
;
import
{
GlIntersectionObserver
}
from
'
@gitlab/ui
'
;
import
{
mount
}
from
'
@vue/test-utils
'
;
import
{
mount
}
from
'
@vue/test-utils
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
{
nextTick
}
from
'
vue
'
;
import
{
useMockIntersectionObserver
}
from
'
helpers/mock_dom_observer
'
;
import
{
useMockIntersectionObserver
}
from
'
helpers/mock_dom_observer
'
;
import
'
~/behaviors/markdown/render_gfm
'
;
import
'
~/behaviors/markdown/render_gfm
'
;
import
IssuableApp
from
'
~/issue_show/components/app.vue
'
;
import
IssuableApp
from
'
~/issue_show/components/app.vue
'
;
...
@@ -17,7 +18,7 @@ import {
...
@@ -17,7 +18,7 @@ import {
publishedIncidentUrl
,
publishedIncidentUrl
,
secondRequest
,
secondRequest
,
zoomMeetingUrl
,
zoomMeetingUrl
,
}
from
'
../mock_data
'
;
}
from
'
../mock_data
/mock_data
'
;
function
formatText
(
text
)
{
function
formatText
(
text
)
{
return
text
.
trim
().
replace
(
/
\s\s
+/g
,
'
'
);
return
text
.
trim
().
replace
(
/
\s\s
+/g
,
'
'
);
...
@@ -36,12 +37,11 @@ describe('Issuable output', () => {
...
@@ -36,12 +37,11 @@ describe('Issuable output', () => {
let
wrapper
;
let
wrapper
;
const
findStickyHeader
=
()
=>
wrapper
.
find
(
'
[data-testid="issue-sticky-header"]
'
);
const
findStickyHeader
=
()
=>
wrapper
.
find
(
'
[data-testid="issue-sticky-header"]
'
);
const
findLockedBadge
=
()
=>
wrapper
.
find
(
'
[data-testid="locked"]
'
);
const
findLockedBadge
=
()
=>
wrapper
.
find
(
'
[data-testid="locked"]
'
);
const
findConfidentialBadge
=
()
=>
wrapper
.
find
(
'
[data-testid="confidential"]
'
);
const
findConfidentialBadge
=
()
=>
wrapper
.
find
(
'
[data-testid="confidential"]
'
);
const
findAlert
=
()
=>
wrapper
.
find
(
'
.alert
'
);
const
mountComponent
=
(
props
=
{},
options
=
{})
=>
{
const
mountComponent
=
(
props
=
{},
options
=
{}
,
data
=
{}
)
=>
{
wrapper
=
mount
(
IssuableApp
,
{
wrapper
=
mount
(
IssuableApp
,
{
propsData
:
{
...
appProps
,
...
props
},
propsData
:
{
...
appProps
,
...
props
},
provide
:
{
provide
:
{
...
@@ -53,6 +53,11 @@ describe('Issuable output', () => {
...
@@ -53,6 +53,11 @@ describe('Issuable output', () => {
HighlightBar
:
true
,
HighlightBar
:
true
,
IncidentTabs
:
true
,
IncidentTabs
:
true
,
},
},
data
()
{
return
{
...
data
,
};
},
...
options
,
...
options
,
});
});
};
};
...
@@ -91,10 +96,8 @@ describe('Issuable output', () => {
...
@@ -91,10 +96,8 @@ describe('Issuable output', () => {
afterEach
(()
=>
{
afterEach
(()
=>
{
mock
.
restore
();
mock
.
restore
();
realtimeRequestCount
=
0
;
realtimeRequestCount
=
0
;
wrapper
.
vm
.
poll
.
stop
();
wrapper
.
vm
.
poll
.
stop
();
wrapper
.
destroy
();
wrapper
.
destroy
();
wrapper
=
null
;
});
});
it
(
'
should render a title/description/edited and update title/description/edited on update
'
,
()
=>
{
it
(
'
should render a title/description/edited and update title/description/edited on update
'
,
()
=>
{
...
@@ -115,7 +118,7 @@ describe('Issuable output', () => {
...
@@ -115,7 +118,7 @@ describe('Issuable output', () => {
expect
(
formatText
(
editedText
.
text
())).
toMatch
(
/Edited
[\s\S]
+
?
by Some User/
);
expect
(
formatText
(
editedText
.
text
())).
toMatch
(
/Edited
[\s\S]
+
?
by Some User/
);
expect
(
editedText
.
find
(
'
.author-link
'
).
attributes
(
'
href
'
)).
toMatch
(
/
\/
some_user$/
);
expect
(
editedText
.
find
(
'
.author-link
'
).
attributes
(
'
href
'
)).
toMatch
(
/
\/
some_user$/
);
expect
(
editedText
.
find
(
'
time
'
).
text
()).
toBeTruthy
();
expect
(
editedText
.
find
(
'
time
'
).
text
()).
toBeTruthy
();
expect
(
wrapper
.
vm
.
state
.
lock_version
).
to
Equal
(
1
);
expect
(
wrapper
.
vm
.
state
.
lock_version
).
to
Be
(
initialRequest
.
lock_version
);
})
})
.
then
(()
=>
{
.
then
(()
=>
{
wrapper
.
vm
.
poll
.
makeRequest
();
wrapper
.
vm
.
poll
.
makeRequest
();
...
@@ -133,7 +136,9 @@ describe('Issuable output', () => {
...
@@ -133,7 +136,9 @@ describe('Issuable output', () => {
expect
(
editedText
.
find
(
'
.author-link
'
).
attributes
(
'
href
'
)).
toMatch
(
/
\/
other_user$/
);
expect
(
editedText
.
find
(
'
.author-link
'
).
attributes
(
'
href
'
)).
toMatch
(
/
\/
other_user$/
);
expect
(
editedText
.
find
(
'
time
'
).
text
()).
toBeTruthy
();
expect
(
editedText
.
find
(
'
time
'
).
text
()).
toBeTruthy
();
expect
(
wrapper
.
vm
.
state
.
lock_version
).
toEqual
(
2
);
// As the lock_version value does not differ from the server,
// we should not see an alert
expect
(
findAlert
().
exists
()).
toBe
(
false
);
});
});
});
});
...
@@ -172,7 +177,7 @@ describe('Issuable output', () => {
...
@@ -172,7 +177,7 @@ describe('Issuable output', () => {
${
'
zoomMeetingUrl
'
}
|
${
zoomMeetingUrl
}
${
'
zoomMeetingUrl
'
}
|
${
zoomMeetingUrl
}
${
'
publishedIncidentUrl
'
}
|
${
publishedIncidentUrl
}
${
'
publishedIncidentUrl
'
}
|
${
publishedIncidentUrl
}
`
(
'
sets the $prop correctly on underlying pinned links
'
,
({
prop
,
value
})
=>
{
`
(
'
sets the $prop correctly on underlying pinned links
'
,
({
prop
,
value
})
=>
{
expect
(
wrapper
.
vm
[
prop
]).
to
Equal
(
value
);
expect
(
wrapper
.
vm
[
prop
]).
to
Be
(
value
);
expect
(
wrapper
.
find
(
`[data-testid="
${
prop
}
"]`
).
attributes
(
'
href
'
)).
toBe
(
value
);
expect
(
wrapper
.
find
(
`[data-testid="
${
prop
}
"]`
).
attributes
(
'
href
'
)).
toBe
(
value
);
});
});
});
});
...
@@ -374,9 +379,9 @@ describe('Issuable output', () => {
...
@@ -374,9 +379,9 @@ describe('Issuable output', () => {
});
});
})
})
.
then
(()
=>
{
.
then
(()
=>
{
expect
(
wrapper
.
vm
.
formState
.
lockedWarningVisible
).
to
Equal
(
true
);
expect
(
wrapper
.
vm
.
formState
.
lockedWarningVisible
).
to
Be
(
true
);
expect
(
wrapper
.
vm
.
formState
.
lock_version
).
to
Equal
(
1
);
expect
(
wrapper
.
vm
.
formState
.
lock_version
).
to
Be
(
1
);
expect
(
wrapper
.
find
(
'
.alert
'
).
exists
()).
toBe
(
true
);
expect
(
findAlert
(
).
exists
()).
toBe
(
true
);
});
});
});
});
});
});
...
@@ -530,7 +535,7 @@ describe('Issuable output', () => {
...
@@ -530,7 +535,7 @@ describe('Issuable output', () => {
`
(
'
$title
'
,
async
({
state
})
=>
{
`
(
'
$title
'
,
async
({
state
})
=>
{
wrapper
.
setProps
({
issuableStatus
:
state
});
wrapper
.
setProps
({
issuableStatus
:
state
});
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
expect
(
findStickyHeader
().
text
()).
toContain
(
IssuableStatusText
[
state
]);
expect
(
findStickyHeader
().
text
()).
toContain
(
IssuableStatusText
[
state
]);
});
});
...
@@ -542,7 +547,7 @@ describe('Issuable output', () => {
...
@@ -542,7 +547,7 @@ describe('Issuable output', () => {
`
(
'
$title
'
,
async
({
isConfidential
})
=>
{
`
(
'
$title
'
,
async
({
isConfidential
})
=>
{
wrapper
.
setProps
({
isConfidential
});
wrapper
.
setProps
({
isConfidential
});
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
expect
(
findConfidentialBadge
().
exists
()).
toBe
(
isConfidential
);
expect
(
findConfidentialBadge
().
exists
()).
toBe
(
isConfidential
);
});
});
...
@@ -554,7 +559,7 @@ describe('Issuable output', () => {
...
@@ -554,7 +559,7 @@ describe('Issuable output', () => {
`
(
'
$title
'
,
async
({
isLocked
})
=>
{
`
(
'
$title
'
,
async
({
isLocked
})
=>
{
wrapper
.
setProps
({
isLocked
});
wrapper
.
setProps
({
isLocked
});
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
expect
(
findLockedBadge
().
exists
()).
toBe
(
isLocked
);
expect
(
findLockedBadge
().
exists
()).
toBe
(
isLocked
);
});
});
...
@@ -562,9 +567,9 @@ describe('Issuable output', () => {
...
@@ -562,9 +567,9 @@ describe('Issuable output', () => {
});
});
describe
(
'
Composable description component
'
,
()
=>
{
describe
(
'
Composable description component
'
,
()
=>
{
const
findIncidentTabs
=
()
=>
wrapper
.
find
(
IncidentTabs
);
const
findIncidentTabs
=
()
=>
wrapper
.
find
Component
(
IncidentTabs
);
const
findDescriptionComponent
=
()
=>
wrapper
.
find
(
DescriptionComponent
);
const
findDescriptionComponent
=
()
=>
wrapper
.
find
Component
(
DescriptionComponent
);
const
findPinnedLinks
=
()
=>
wrapper
.
find
(
PinnedLinks
);
const
findPinnedLinks
=
()
=>
wrapper
.
find
Component
(
PinnedLinks
);
const
borderClass
=
'
gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6
'
;
const
borderClass
=
'
gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6
'
;
describe
(
'
when using description component
'
,
()
=>
{
describe
(
'
when using description component
'
,
()
=>
{
...
...
spec/frontend/issue_show/components/description_spec.js
View file @
afe6e9e1
...
@@ -5,7 +5,7 @@ import { TEST_HOST } from 'helpers/test_constants';
...
@@ -5,7 +5,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import
mountComponent
from
'
helpers/vue_mount_component_helper
'
;
import
mountComponent
from
'
helpers/vue_mount_component_helper
'
;
import
Description
from
'
~/issue_show/components/description.vue
'
;
import
Description
from
'
~/issue_show/components/description.vue
'
;
import
TaskList
from
'
~/task_list
'
;
import
TaskList
from
'
~/task_list
'
;
import
{
descriptionProps
as
props
}
from
'
../mock_data
'
;
import
{
descriptionProps
as
props
}
from
'
../mock_data
/mock_data
'
;
jest
.
mock
(
'
~/task_list
'
);
jest
.
mock
(
'
~/task_list
'
);
...
...
spec/frontend/issue_show/components/edit_actions_spec.js
View file @
afe6e9e1
import
Vue
from
'
vue
'
;
import
{
GlButton
,
GlModal
}
from
'
@gitlab/ui
'
;
import
editActions
from
'
~/issue_show/components/edit_actions.vue
'
;
import
{
createLocalVue
}
from
'
@vue/test-utils
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
IssuableEditActions
from
'
~/issue_show/components/edit_actions.vue
'
;
import
eventHub
from
'
~/issue_show/event_hub
'
;
import
eventHub
from
'
~/issue_show/event_hub
'
;
import
Store
from
'
~/issue_show/stores
'
;
describe
(
'
Edit Actions components
'
,
()
=>
{
import
{
let
vm
;
getIssueStateQueryResponse
,
updateIssueStateQueryResponse
,
}
from
'
../mock_data/apollo_mock
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
VueApollo
);
describe
(
'
Edit Actions component
'
,
()
=>
{
let
wrapper
;
let
fakeApollo
;
let
mockIssueStateData
;
const
mockResolvers
=
{
Query
:
{
issueState
()
{
return
{
__typename
:
'
IssueState
'
,
rawData
:
mockIssueStateData
(),
};
},
},
};
beforeEach
((
done
)
=>
{
const
modalId
=
'
delete-issuable-modal-1
'
;
const
Component
=
Vue
.
extend
(
editActions
);
const
store
=
new
Store
({
titleHtml
:
''
,
descriptionHtml
:
''
,
issuableRef
:
''
,
});
store
.
formState
.
title
=
'
test
'
;
jest
.
spyOn
(
eventHub
,
'
$emit
'
).
mockImplementation
(()
=>
{});
const
createComponent
=
({
props
,
data
}
=
{})
=>
{
fakeApollo
=
createMockApollo
([],
mockResolvers
);
vm
=
new
Component
({
wrapper
=
shallowMountExtended
(
IssuableEditActions
,
{
apolloProvider
:
fakeApollo
,
propsData
:
{
propsData
:
{
formState
:
{
title
:
'
GitLab Issue
'
,
},
canDestroy
:
true
,
canDestroy
:
true
,
formState
:
store
.
formState
,
issuableType
:
'
issue
'
,
issuableType
:
'
issue
'
,
...
props
,
},
},
}).
$mount
();
data
()
{
return
{
issueState
:
{},
modalId
,
...
data
,
};
},
});
};
Vue
.
nextTick
(
done
);
async
function
deleteIssuable
(
localWrapper
)
{
});
localWrapper
.
findComponent
(
GlModal
).
vm
.
$emit
(
'
primary
'
);
}
it
(
'
renders all buttons as enabled
'
,
()
=>
{
const
findModal
=
()
=>
wrapper
.
findComponent
(
GlModal
);
expect
(
vm
.
$el
.
querySelectorAll
(
'
.disabled
'
).
length
).
toBe
(
0
);
const
findEditButtons
=
()
=>
wrapper
.
findAllComponents
(
GlButton
);
const
findDeleteButton
=
()
=>
wrapper
.
findByTestId
(
'
issuable-delete-button
'
);
const
findSaveButton
=
()
=>
wrapper
.
findByTestId
(
'
issuable-save-button
'
);
const
findCancelButton
=
()
=>
wrapper
.
findByTestId
(
'
issuable-cancel-button
'
);
expect
(
vm
.
$el
.
querySelectorAll
(
'
[disabled]
'
).
length
).
toBe
(
0
);
beforeEach
(()
=>
{
mockIssueStateData
=
jest
.
fn
();
createComponent
();
});
});
it
(
'
does not render delete button if canUpdate is false
'
,
(
done
)
=>
{
afterEach
(()
=>
{
vm
.
canDestroy
=
false
;
wrapper
.
destroy
();
});
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.btn-danger
'
)).
toBeNull
();
done
();
it
(
'
renders all buttons as enabled
'
,
()
=>
{
const
buttons
=
findEditButtons
().
wrappers
;
buttons
.
forEach
((
button
)
=>
{
expect
(
button
.
attributes
(
'
disabled
'
)).
toBeFalsy
();
});
});
});
});
it
(
'
disables submit button when title is blank
'
,
(
done
)
=>
{
it
(
'
does not render the delete button if canDestroy is false
'
,
()
=>
{
vm
.
formState
.
title
=
''
;
createComponent
({
props
:
{
canDestroy
:
false
}
});
expect
(
findDeleteButton
().
exists
()).
toBe
(
false
);
});
Vue
.
nextTick
(
()
=>
{
it
(
'
disables save button when title is blank
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.btn-confirm
'
).
getAttribute
(
'
disabled
'
)).
toBe
(
'
disabled
'
);
createComponent
({
props
:
{
formState
:
{
title
:
''
,
issue_type
:
''
}
}
}
);
done
();
expect
(
findSaveButton
().
attributes
(
'
disabled
'
)).
toBe
(
'
true
'
);
});
});
});
it
(
'
should not show delete button if showDeleteButton is false
'
,
(
done
)
=>
{
it
(
'
does not render the delete button if showDeleteButton is false
'
,
(
)
=>
{
vm
.
showDeleteButton
=
false
;
createComponent
({
props
:
{
showDeleteButton
:
false
}
})
;
Vue
.
nextTick
(()
=>
{
expect
(
findDeleteButton
().
exists
()).
toBe
(
false
);
expect
(
vm
.
$el
.
querySelector
(
'
.btn-danger
'
)).
toBeNull
();
done
();
});
});
});
describe
(
'
updateIssuable
'
,
()
=>
{
describe
(
'
updateIssuable
'
,
()
=>
{
it
(
'
sends update.issauble event when clicking save button
'
,
()
=>
{
beforeEach
(()
=>
{
vm
.
$el
.
querySelector
(
'
.btn-confirm
'
).
click
();
jest
.
spyOn
(
eventHub
,
'
$emit
'
).
mockImplementation
(()
=>
{});
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
update.issuable
'
);
});
});
it
(
'
disabled button after clicking save button
'
,
(
done
)
=>
{
it
(
'
sends update.issauble event when clicking save button
'
,
()
=>
{
vm
.
$el
.
querySelector
(
'
.btn-confirm
'
).
click
();
findSaveButton
().
vm
.
$emit
(
'
click
'
,
{
preventDefault
:
jest
.
fn
()
});
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.btn-confirm
'
).
getAttribute
(
'
disabled
'
)).
toBe
(
'
disabled
'
);
done
();
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
update.issuable
'
);
});
});
});
});
});
describe
(
'
closeForm
'
,
()
=>
{
describe
(
'
closeForm
'
,
()
=>
{
beforeEach
(()
=>
{
jest
.
spyOn
(
eventHub
,
'
$emit
'
).
mockImplementation
(()
=>
{});
});
it
(
'
emits close.form when clicking cancel
'
,
()
=>
{
it
(
'
emits close.form when clicking cancel
'
,
()
=>
{
vm
.
$el
.
querySelector
(
'
.btn-default
'
).
click
(
);
findCancelButton
().
vm
.
$emit
(
'
click
'
);
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
close.form
'
);
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
close.form
'
);
});
});
});
});
describe
(
'
deleteIssuable
'
,
()
=>
{
describe
(
'
renders create modal with the correct information
'
,
()
=>
{
it
(
'
sends delete.issuable event when clicking save button
'
,
()
=>
{
it
(
'
renders correct modal id
'
,
()
=>
{
jest
.
spyOn
(
window
,
'
confirm
'
).
mockReturnValue
(
true
);
expect
(
findModal
().
attributes
(
'
modalid
'
)).
toBe
(
modalId
);
vm
.
$el
.
querySelector
(
'
.btn-danger
'
).
click
();
});
});
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
delete.issuable
'
,
{
destroy_confirm
:
true
});
describe
(
'
deleteIssuable
'
,
()
=>
{
beforeEach
(()
=>
{
jest
.
spyOn
(
eventHub
,
'
$emit
'
).
mockImplementation
(()
=>
{});
});
});
it
(
'
does no actions when confirm is false
'
,
(
done
)
=>
{
it
(
'
does not send the `delete.issuable` event when clicking delete button
'
,
()
=>
{
jest
.
spyOn
(
window
,
'
confirm
'
).
mockReturnValue
(
false
);
findDeleteButton
().
vm
.
$emit
(
'
click
'
);
vm
.
$el
.
querySelector
(
'
.btn-danger
'
).
click
();
expect
(
eventHub
.
$emit
).
not
.
toHaveBeenCalled
();
});
Vue
.
nextTick
(()
=>
{
it
(
'
sends the `delete.issuable` event when clicking the delete confirm button
'
,
async
()
=>
{
expect
(
eventHub
.
$emit
).
not
.
toHaveBeenCalledWith
(
'
delete.issuable
'
);
expect
(
eventHub
.
$emit
).
toHaveBeenCalledTimes
(
0
);
await
deleteIssuable
(
wrapper
);
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
delete.issuable
'
,
{
destroy_confirm
:
true
});
expect
(
eventHub
.
$emit
).
toHaveBeenCalledTimes
(
1
);
});
});
expect
(
vm
.
$el
.
querySelector
(
'
.btn-danger .fa
'
)).
toBeNull
();
describe
(
'
with Apollo cache mock
'
,
()
=>
{
it
(
'
renders the right delete button text per apollo cache type
'
,
async
()
=>
{
mockIssueStateData
.
mockResolvedValue
(
getIssueStateQueryResponse
);
await
waitForPromises
();
expect
(
findDeleteButton
().
text
()).
toBe
(
'
Delete issue
'
);
});
done
();
it
(
'
should not change the delete button text per apollo cache mutation
'
,
async
()
=>
{
});
mockIssueStateData
.
mockResolvedValue
(
updateIssueStateQueryResponse
);
await
waitForPromises
();
expect
(
findDeleteButton
().
text
()).
toBe
(
'
Delete issue
'
);
});
});
});
});
});
});
spec/frontend/issue_show/components/fields/type_spec.js
0 → 100644
View file @
afe6e9e1
import
{
GlFormGroup
,
GlDropdown
,
GlDropdownItem
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
IssueTypeField
,
{
i18n
}
from
'
~/issue_show/components/fields/type.vue
'
;
import
{
IssuableTypes
}
from
'
~/issue_show/constants
'
;
import
{
getIssueStateQueryResponse
,
updateIssueStateQueryResponse
,
}
from
'
../../mock_data/apollo_mock
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
VueApollo
);
describe
(
'
Issue type field component
'
,
()
=>
{
let
wrapper
;
let
fakeApollo
;
let
mockIssueStateData
;
const
mockResolvers
=
{
Query
:
{
issueState
()
{
return
{
__typename
:
'
IssueState
'
,
rawData
:
mockIssueStateData
(),
};
},
},
Mutation
:
{
updateIssueState
:
jest
.
fn
().
mockResolvedValue
(
updateIssueStateQueryResponse
),
},
};
const
findTypeFromGroup
=
()
=>
wrapper
.
findComponent
(
GlFormGroup
);
const
findTypeFromDropDown
=
()
=>
wrapper
.
findComponent
(
GlDropdown
);
const
findTypeFromDropDownItems
=
()
=>
wrapper
.
findAllComponents
(
GlDropdownItem
);
const
createComponent
=
({
data
}
=
{})
=>
{
fakeApollo
=
createMockApollo
([],
mockResolvers
);
wrapper
=
shallowMount
(
IssueTypeField
,
{
localVue
,
apolloProvider
:
fakeApollo
,
data
()
{
return
{
issueState
:
{},
...
data
,
};
},
});
};
beforeEach
(()
=>
{
mockIssueStateData
=
jest
.
fn
();
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
(
'
renders a form group with the correct label
'
,
()
=>
{
expect
(
findTypeFromGroup
().
attributes
(
'
label
'
)).
toBe
(
i18n
.
label
);
});
it
(
'
renders a form select with the `issue_type` value
'
,
()
=>
{
expect
(
findTypeFromDropDown
().
attributes
(
'
value
'
)).
toBe
(
IssuableTypes
.
issue
);
});
describe
(
'
with Apollo cache mock
'
,
()
=>
{
it
(
'
renders the selected issueType
'
,
async
()
=>
{
mockIssueStateData
.
mockResolvedValue
(
getIssueStateQueryResponse
);
await
waitForPromises
();
expect
(
findTypeFromDropDown
().
attributes
(
'
value
'
)).
toBe
(
IssuableTypes
.
issue
);
});
it
(
'
updates the `issue_type` in the apollo cache when the value is changed
'
,
async
()
=>
{
findTypeFromDropDownItems
().
at
(
1
).
vm
.
$emit
(
'
click
'
,
IssuableTypes
.
incident
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
findTypeFromDropDown
().
attributes
(
'
value
'
)).
toBe
(
IssuableTypes
.
incident
);
});
});
});
spec/frontend/issue_show/components/form_spec.js
View file @
afe6e9e1
...
@@ -2,6 +2,7 @@ import { GlAlert } from '@gitlab/ui';
...
@@ -2,6 +2,7 @@ import { GlAlert } from '@gitlab/ui';
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
Autosave
from
'
~/autosave
'
;
import
Autosave
from
'
~/autosave
'
;
import
DescriptionTemplate
from
'
~/issue_show/components/fields/description_template.vue
'
;
import
DescriptionTemplate
from
'
~/issue_show/components/fields/description_template.vue
'
;
import
IssueTypeField
from
'
~/issue_show/components/fields/type.vue
'
;
import
formComponent
from
'
~/issue_show/components/form.vue
'
;
import
formComponent
from
'
~/issue_show/components/form.vue
'
;
import
LockedWarning
from
'
~/issue_show/components/locked_warning.vue
'
;
import
LockedWarning
from
'
~/issue_show/components/locked_warning.vue
'
;
import
eventHub
from
'
~/issue_show/event_hub
'
;
import
eventHub
from
'
~/issue_show/event_hub
'
;
...
@@ -39,6 +40,7 @@ describe('Inline edit form component', () => {
...
@@ -39,6 +40,7 @@ describe('Inline edit form component', () => {
};
};
const
findDescriptionTemplate
=
()
=>
wrapper
.
findComponent
(
DescriptionTemplate
);
const
findDescriptionTemplate
=
()
=>
wrapper
.
findComponent
(
DescriptionTemplate
);
const
findIssuableTypeField
=
()
=>
wrapper
.
findComponent
(
IssueTypeField
);
const
findLockedWarning
=
()
=>
wrapper
.
findComponent
(
LockedWarning
);
const
findLockedWarning
=
()
=>
wrapper
.
findComponent
(
LockedWarning
);
const
findAlert
=
()
=>
wrapper
.
findComponent
(
GlAlert
);
const
findAlert
=
()
=>
wrapper
.
findComponent
(
GlAlert
);
...
@@ -68,6 +70,21 @@ describe('Inline edit form component', () => {
...
@@ -68,6 +70,21 @@ describe('Inline edit form component', () => {
expect
(
findDescriptionTemplate
().
exists
()).
toBe
(
true
);
expect
(
findDescriptionTemplate
().
exists
()).
toBe
(
true
);
});
});
it
.
each
`
issuableType | value
${
'
issue
'
}
|
${
true
}
${
'
epic
'
}
|
${
false
}
`
(
'
when `issue_type` is set to "$issuableType" rendering the type select will be "$value"
'
,
({
issuableType
,
value
})
=>
{
createComponent
({
issuableType
,
});
expect
(
findIssuableTypeField
().
exists
()).
toBe
(
value
);
},
);
it
(
'
hides locked warning by default
'
,
()
=>
{
it
(
'
hides locked warning by default
'
,
()
=>
{
createComponent
();
createComponent
();
...
...
spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
View file @
afe6e9e1
...
@@ -9,7 +9,7 @@ import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
...
@@ -9,7 +9,7 @@ import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
import
INVALID_URL
from
'
~/lib/utils/invalid_url
'
;
import
INVALID_URL
from
'
~/lib/utils/invalid_url
'
;
import
Tracking
from
'
~/tracking
'
;
import
Tracking
from
'
~/tracking
'
;
import
AlertDetailsTable
from
'
~/vue_shared/components/alert_details_table.vue
'
;
import
AlertDetailsTable
from
'
~/vue_shared/components/alert_details_table.vue
'
;
import
{
descriptionProps
}
from
'
../../mock_data
'
;
import
{
descriptionProps
}
from
'
../../mock_data
/mock_data
'
;
const
mockAlert
=
{
const
mockAlert
=
{
__typename
:
'
AlertManagementAlert
'
,
__typename
:
'
AlertManagementAlert
'
,
...
...
spec/frontend/issue_show/issue_spec.js
View file @
afe6e9e1
...
@@ -5,7 +5,7 @@ import { initIssuableApp } from '~/issue_show/issue';
...
@@ -5,7 +5,7 @@ import { initIssuableApp } from '~/issue_show/issue';
import
*
as
parseData
from
'
~/issue_show/utils/parse_data
'
;
import
*
as
parseData
from
'
~/issue_show/utils/parse_data
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
createStore
from
'
~/notes/stores
'
;
import
createStore
from
'
~/notes/stores
'
;
import
{
appProps
}
from
'
./mock_data
'
;
import
{
appProps
}
from
'
./mock_data
/mock_data
'
;
const
mock
=
new
MockAdapter
(
axios
);
const
mock
=
new
MockAdapter
(
axios
);
mock
.
onGet
().
reply
(
200
);
mock
.
onGet
().
reply
(
200
);
...
...
spec/frontend/issue_show/mock_data/apollo_mock.js
0 → 100644
View file @
afe6e9e1
export
const
getIssueStateQueryResponse
=
{
issueType
:
'
issue
'
,
isDirty
:
false
,
};
export
const
updateIssueStateQueryResponse
=
{
issueType
:
'
incident
'
,
isDirty
:
true
,
};
spec/frontend/issue_show/mock_data.js
→
spec/frontend/issue_show/mock_data
/mock_data
.js
View file @
afe6e9e1
...
@@ -48,6 +48,7 @@ export const appProps = {
...
@@ -48,6 +48,7 @@ export const appProps = {
initialDescriptionHtml
:
'
test
'
,
initialDescriptionHtml
:
'
test
'
,
initialDescriptionText
:
'
test
'
,
initialDescriptionText
:
'
test
'
,
lockVersion
:
1
,
lockVersion
:
1
,
issueType
:
'
issue
'
,
markdownPreviewPath
:
'
/
'
,
markdownPreviewPath
:
'
/
'
,
markdownDocsPath
:
'
/
'
,
markdownDocsPath
:
'
/
'
,
projectNamespace
:
'
/
'
,
projectNamespace
:
'
/
'
,
...
...
spec/services/issues/create_service_spec.rb
View file @
afe6e9e1
...
@@ -78,8 +78,8 @@ RSpec.describe Issues::CreateService do
...
@@ -78,8 +78,8 @@ RSpec.describe Issues::CreateService do
opts
.
merge!
(
title:
''
)
opts
.
merge!
(
title:
''
)
end
end
it
'does not
create
an incident label prematurely'
do
it
'does not
apply
an incident label prematurely'
do
expect
{
subject
}.
not_to
change
(
Label
,
:count
)
expect
{
subject
}.
to
not_change
(
LabelLink
,
:count
).
and
not_change
(
Issue
,
:count
)
end
end
end
end
end
end
...
...
spec/services/issues/update_service_spec.rb
View file @
afe6e9e1
...
@@ -158,6 +158,90 @@ RSpec.describe Issues::UpdateService, :mailer do
...
@@ -158,6 +158,90 @@ RSpec.describe Issues::UpdateService, :mailer do
end
end
end
end
context
'changing issue_type'
do
let!
(
:label_1
)
{
create
(
:label
,
project:
project
,
title:
'incident'
)
}
let!
(
:label_2
)
{
create
(
:label
,
project:
project
,
title:
'missed-sla'
)
}
before
do
stub_licensed_features
(
quality_management:
true
)
end
context
'from issue to incident'
do
it
'adds a `incident` label if one does not exist'
do
expect
{
update_issue
(
issue_type:
'incident'
)
}.
to
change
(
issue
.
labels
,
:count
).
by
(
1
)
expect
(
issue
.
labels
.
pluck
(
:title
)).
to
eq
([
'incident'
])
end
context
'for an issue with multiple labels'
do
let
(
:issue
)
{
create
(
:incident
,
project:
project
,
labels:
[
label_1
])
}
before
do
update_issue
(
issue_type:
'incident'
)
end
it
'does not add an `incident` label if one already exist'
do
expect
(
issue
.
labels
).
to
eq
([
label_1
])
end
end
context
'filtering the incident label'
do
let
(
:params
)
{
{
add_label_ids:
[]
}
}
before
do
update_issue
(
issue_type:
'incident'
)
end
it
'creates and add a incident label id to add_label_ids'
do
expect
(
issue
.
label_ids
).
to
contain_exactly
(
label_1
.
id
)
end
end
end
context
'from incident to issue'
do
let
(
:issue
)
{
create
(
:incident
,
project:
project
)
}
context
'for an incident with multiple labels'
do
let
(
:issue
)
{
create
(
:incident
,
project:
project
,
labels:
[
label_1
,
label_2
])
}
before
do
update_issue
(
issue_type:
'issue'
)
end
it
'removes an `incident` label if one exists on the incident'
do
expect
(
issue
.
labels
).
to
eq
([
label_2
])
end
end
context
'filtering the incident label'
do
let
(
:issue
)
{
create
(
:incident
,
project:
project
,
labels:
[
label_1
,
label_2
])
}
let
(
:params
)
{
{
label_ids:
[
label_1
.
id
,
label_2
.
id
],
remove_label_ids:
[]
}
}
before
do
update_issue
(
issue_type:
'issue'
)
end
it
'adds an incident label id to remove_label_ids for it to be removed'
do
expect
(
issue
.
label_ids
).
to
contain_exactly
(
label_2
.
id
)
end
end
end
context
'from issue to restricted issue types'
do
context
'without sufficient permissions'
do
let
(
:user
)
{
create
(
:user
)
}
before
do
project
.
add_guest
(
user
)
end
it
'does nothing to the labels'
do
expect
{
update_issue
(
issue_type:
'issue'
)
}.
not_to
change
(
issue
.
labels
,
:count
)
expect
(
issue
.
reload
.
labels
).
to
eq
([])
end
end
end
end
it
'updates open issue counter for assignees when issue is reassigned'
do
it
'updates open issue counter for assignees when issue is reassigned'
do
update_issue
(
assignee_ids:
[
user2
.
id
])
update_issue
(
assignee_ids:
[
user2
.
id
])
...
...
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