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
f4f206b5
Commit
f4f206b5
authored
Jan 06, 2021
by
Axel Garcia
Committed by
Enrique Alcántara
Jan 06, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Edit issue title in swimlanes sidebar
This adds an inline form to update the active issue title
parent
198349ee
Changes
12
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
557 additions
and
20 deletions
+557
-20
app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
...scripts/boards/components/sidebar/board_editable_item.vue
+32
-5
app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue
...s/boards/components/sidebar/board_sidebar_issue_title.vue
+171
-0
app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql
...vascripts/boards/graphql/issue_set_title.mutation.graphql
+8
-0
app/assets/javascripts/boards/stores/actions.js
app/assets/javascripts/boards/stores/actions.js
+25
-0
doc/user/project/issue_board.md
doc/user/project/issue_board.md
+15
-1
ee/app/assets/javascripts/boards/components/board_content_sidebar.vue
...s/javascripts/boards/components/board_content_sidebar.vue
+4
-5
ee/changelogs/unreleased/232745-edit-issue-title-in-swimlanes-sidebar.yml
...released/232745-edit-issue-title-in-swimlanes-sidebar.yml
+5
-0
ee/spec/frontend/boards/components/board_content_sidebar_spec.js
.../frontend/boards/components/board_content_sidebar_spec.js
+27
-6
locale/gitlab.pot
locale/gitlab.pot
+15
-0
spec/frontend/boards/components/sidebar/board_editable_item_spec.js
...end/boards/components/sidebar/board_editable_item_spec.js
+22
-3
spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js
...ards/components/sidebar/board_sidebar_issue_title_spec.js
+182
-0
spec/frontend/boards/stores/actions_spec.js
spec/frontend/boards/stores/actions_spec.js
+51
-0
No files found.
app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
View file @
f4f206b5
...
...
@@ -14,6 +14,16 @@ export default {
required
:
false
,
default
:
false
,
},
toggleHeader
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
handleOffClick
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
},
},
inject
:
[
'
canUpdate
'
],
data
()
{
...
...
@@ -21,13 +31,25 @@ export default {
edit
:
false
,
};
},
computed
:
{
showHeader
()
{
if
(
!
this
.
toggleHeader
)
{
return
true
;
}
return
!
this
.
edit
;
},
},
destroyed
()
{
window
.
removeEventListener
(
'
click
'
,
this
.
collapseWhenOffClick
);
},
methods
:
{
collapseWhenOffClick
({
target
})
{
if
(
!
this
.
$el
.
contains
(
target
))
{
this
.
collapse
();
this
.
$emit
(
'
off-click
'
);
if
(
this
.
handleOffClick
)
{
this
.
collapse
();
}
}
},
expand
()
{
...
...
@@ -63,21 +85,26 @@ export default {
<
template
>
<div>
<div
class=
"gl-display-flex gl-justify-content-space-between gl-mb-3"
>
<header
v-show=
"showHeader"
class=
"gl-display-flex gl-justify-content-space-between gl-align-items-flex-start gl-mb-3"
>
<span
class=
"gl-vertical-align-middle"
>
<span
data-testid=
"title"
>
{{
title
}}
</span>
<slot
name=
"title"
>
<span
data-testid=
"title"
>
{{
title
}}
</span>
</slot>
<gl-loading-icon
v-if=
"loading"
inline
class=
"gl-ml-2"
/>
</span>
<gl-button
v-if=
"canUpdate"
variant=
"link"
class=
"gl-text-gray-900! js-sidebar-dropdown-toggle"
class=
"gl-text-gray-900!
gl-ml-5
js-sidebar-dropdown-toggle"
data-testid=
"edit-button"
@
click=
"toggle"
>
{{
__
(
'
Edit
'
)
}}
</gl-button>
</
div
>
</
header
>
<div
v-show=
"!edit"
class=
"gl-text-gray-500"
data-testid=
"collapsed-content"
>
<slot
name=
"collapsed"
>
{{
__
(
'
None
'
)
}}
</slot>
</div>
...
...
app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue
0 → 100644
View file @
f4f206b5
<
script
>
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
{
GlAlert
,
GlButton
,
GlForm
,
GlFormGroup
,
GlFormInput
}
from
'
@gitlab/ui
'
;
import
BoardEditableItem
from
'
~/boards/components/sidebar/board_editable_item.vue
'
;
import
autofocusonshow
from
'
~/vue_shared/directives/autofocusonshow
'
;
import
{
joinPaths
}
from
'
~/lib/utils/url_utility
'
;
import
createFlash
from
'
~/flash
'
;
import
{
__
}
from
'
~/locale
'
;
export
default
{
components
:
{
GlForm
,
GlAlert
,
GlButton
,
GlFormGroup
,
GlFormInput
,
BoardEditableItem
,
},
directives
:
{
autofocusonshow
,
},
data
()
{
return
{
title
:
''
,
loading
:
false
,
showChangesAlert
:
false
,
};
},
computed
:
{
...
mapGetters
({
issue
:
'
activeIssue
'
}),
pendingChangesStorageKey
()
{
return
this
.
getPendingChangesKey
(
this
.
issue
);
},
projectPath
()
{
const
referencePath
=
this
.
issue
.
referencePath
||
''
;
return
referencePath
.
slice
(
0
,
referencePath
.
indexOf
(
'
#
'
));
},
validationState
()
{
return
Boolean
(
this
.
title
);
},
},
watch
:
{
issue
:
{
handler
(
updatedIssue
,
formerIssue
)
{
if
(
formerIssue
?.
title
!==
this
.
title
)
{
localStorage
.
setItem
(
this
.
getPendingChangesKey
(
formerIssue
),
this
.
title
);
}
this
.
title
=
updatedIssue
.
title
;
this
.
setPendingState
();
},
immediate
:
true
,
},
},
methods
:
{
...
mapActions
([
'
setActiveIssueTitle
'
]),
getPendingChangesKey
(
issue
)
{
if
(
!
issue
)
{
return
''
;
}
return
joinPaths
(
window
.
location
.
pathname
.
slice
(
1
),
String
(
issue
.
id
),
'
issue-title-pending-changes
'
,
);
},
async
setPendingState
()
{
const
pendingChanges
=
localStorage
.
getItem
(
this
.
pendingChangesStorageKey
);
if
(
pendingChanges
)
{
this
.
title
=
pendingChanges
;
this
.
showChangesAlert
=
true
;
await
this
.
$nextTick
();
this
.
$refs
.
sidebarItem
.
expand
();
}
else
{
this
.
showChangesAlert
=
false
;
}
},
cancel
()
{
this
.
title
=
this
.
issue
.
title
;
this
.
$refs
.
sidebarItem
.
collapse
();
this
.
showChangesAlert
=
false
;
localStorage
.
removeItem
(
this
.
pendingChangesStorageKey
);
},
async
setTitle
()
{
this
.
$refs
.
sidebarItem
.
collapse
();
if
(
!
this
.
title
||
this
.
title
===
this
.
issue
.
title
)
{
return
;
}
try
{
this
.
loading
=
true
;
await
this
.
setActiveIssueTitle
({
title
:
this
.
title
,
projectPath
:
this
.
projectPath
});
localStorage
.
removeItem
(
this
.
pendingChangesStorageKey
);
this
.
showChangesAlert
=
false
;
}
catch
(
e
)
{
this
.
title
=
this
.
issue
.
title
;
createFlash
({
message
:
this
.
$options
.
i18n
.
updateTitleError
});
}
finally
{
this
.
loading
=
false
;
}
},
handleOffClick
()
{
if
(
this
.
title
!==
this
.
issue
.
title
)
{
this
.
showChangesAlert
=
true
;
localStorage
.
setItem
(
this
.
pendingChangesStorageKey
,
this
.
title
);
}
else
{
this
.
$refs
.
sidebarItem
.
collapse
();
}
},
},
i18n
:
{
issueTitlePlaceholder
:
__
(
'
Issue title
'
),
submitButton
:
__
(
'
Save changes
'
),
cancelButton
:
__
(
'
Cancel
'
),
updateTitleError
:
__
(
'
An error occurred when updating the issue title
'
),
invalidFeedback
:
__
(
'
An issue title is required
'
),
reviewYourChanges
:
__
(
'
Changes to the title have not been saved
'
),
},
};
</
script
>
<
template
>
<board-editable-item
ref=
"sidebarItem"
toggle-header
:loading=
"loading"
:handle-off-click=
"false"
@
off-click=
"handleOffClick"
>
<template
#title
>
<span
class=
"gl-font-weight-bold"
data-testid=
"issue-title"
>
{{
issue
.
title
}}
</span>
</
template
>
<
template
#collapsed
>
<span
class=
"gl-text-gray-800"
>
{{
issue
.
referencePath
}}
</span>
</
template
>
<
template
>
<gl-alert
v-if=
"showChangesAlert"
variant=
"warning"
class=
"gl-mb-5"
:dismissible=
"false"
>
{{
$options
.
i18n
.
reviewYourChanges
}}
</gl-alert>
<gl-form
@
submit.prevent=
"setTitle"
>
<gl-form-group
:invalid-feedback=
"$options.i18n.invalidFeedback"
:state=
"validationState"
>
<gl-form-input
v-model=
"title"
v-autofocusonshow
:placeholder=
"$options.i18n.issueTitlePlaceholder"
:state=
"validationState"
/>
</gl-form-group>
<div
class=
"gl-display-flex gl-w-full gl-justify-content-space-between gl-mt-5"
>
<gl-button
variant=
"success"
size=
"small"
data-testid=
"submit-button"
:disabled=
"!title"
@
click=
"setTitle"
>
{{
$options
.
i18n
.
submitButton
}}
</gl-button>
<gl-button
size=
"small"
data-testid=
"cancel-button"
@
click=
"cancel"
>
{{
$options
.
i18n
.
cancelButton
}}
</gl-button>
</div>
</gl-form>
</
template
>
</board-editable-item>
</template>
app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql
0 → 100644
View file @
f4f206b5
mutation
issueSetTitle
(
$input
:
UpdateIssueInput
!)
{
updateIssue
(
input
:
$input
)
{
issue
{
title
}
errors
}
}
app/assets/javascripts/boards/stores/actions.js
View file @
f4f206b5
...
...
@@ -27,6 +27,7 @@ import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql
import
issueSetDueDateMutation
from
'
../graphql/issue_set_due_date.mutation.graphql
'
;
import
issueSetSubscriptionMutation
from
'
../graphql/issue_set_subscription.mutation.graphql
'
;
import
issueSetMilestoneMutation
from
'
../graphql/issue_set_milestone.mutation.graphql
'
;
import
issueSetTitleMutation
from
'
../graphql/issue_set_title.mutation.graphql
'
;
const
notImplemented
=
()
=>
{
/* eslint-disable-next-line @gitlab/require-i18n-strings */
...
...
@@ -472,6 +473,30 @@ export default {
});
},
setActiveIssueTitle
:
async
({
commit
,
getters
},
input
)
=>
{
const
{
activeIssue
}
=
getters
;
const
{
data
}
=
await
gqlClient
.
mutate
({
mutation
:
issueSetTitleMutation
,
variables
:
{
input
:
{
iid
:
String
(
activeIssue
.
iid
),
projectPath
:
input
.
projectPath
,
title
:
input
.
title
,
},
},
});
if
(
data
.
updateIssue
?.
errors
?.
length
>
0
)
{
throw
new
Error
(
data
.
updateIssue
.
errors
);
}
commit
(
types
.
UPDATE_ISSUE_BY_ID
,
{
issueId
:
activeIssue
.
id
,
prop
:
'
title
'
,
value
:
data
.
updateIssue
.
issue
.
title
,
});
},
fetchBacklog
:
()
=>
{
notImplemented
();
},
...
...
doc/user/project/issue_board.md
View file @
f4f206b5
...
...
@@ -321,7 +321,8 @@ As in other list types, click the trash icon to remove a list.
### Group issues in swimlanes **(PREMIUM)**
> Grouping by epic [introduced](https://gitlab.com/groups/gitlab-org/-/epics/3352) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.6.
> - Grouping by epic [introduced](https://gitlab.com/groups/gitlab-org/-/epics/3352) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.6.
> - Editing issue titles in the issue sidebar [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232745) in GitLab 13.8.
With swimlanes you can visualize issues grouped by epic.
Your issue board keeps all the other features, but with a different visual organization of issues.
...
...
@@ -337,6 +338,19 @@ To group issues by epic in an issue board:
![
Epics Swimlanes
](
img/epics_swimlanes_v13.6.png
)
To edit an issue without leaving this view, select the issue card (not its title), and a sidebar
appears on the right. There you can see and edit the issue's:
-
Title
-
Assignees
-
Epic
**PREMIUM**
-
Milestone
-
Time tracking value (view only)
-
Due date
-
Labels
-
Weight
-
Notifications setting
You can also
[
drag issues
](
#drag-issues-between-lists
)
to change their position and epic assignment:
-
To reorder an issue, drag it to the new position within a list.
...
...
ee/app/assets/javascripts/boards/components/board_content_sidebar.vue
View file @
f4f206b5
...
...
@@ -3,13 +3,13 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import
{
GlDrawer
}
from
'
@gitlab/ui
'
;
import
{
ISSUABLE
}
from
'
~/boards/constants
'
;
import
{
contentTop
}
from
'
~/lib/utils/common_utils
'
;
import
IssuableTitle
from
'
~/boards/components/issuable_title.vue
'
;
import
glFeatureFlagsMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
BoardSidebarEpicSelect
from
'
./sidebar/board_sidebar_epic_select.vue
'
;
import
BoardAssigneeDropdown
from
'
~/boards/components/board_assignee_dropdown.vue
'
;
import
BoardSidebarTimeTracker
from
'
./sidebar/board_sidebar_time_tracker.vue
'
;
import
BoardSidebarWeightInput
from
'
./sidebar/board_sidebar_weight_input.vue
'
;
import
BoardSidebarLabelsSelect
from
'
~/boards/components/sidebar/board_sidebar_labels_select.vue
'
;
import
BoardSidebarIssueTitle
from
'
~/boards/components/sidebar/board_sidebar_issue_title.vue
'
;
import
BoardSidebarDueDate
from
'
~/boards/components/sidebar/board_sidebar_due_date.vue
'
;
import
BoardSidebarSubscription
from
'
~/boards/components/sidebar/board_sidebar_subscription.vue
'
;
import
BoardSidebarMilestoneSelect
from
'
~/boards/components/sidebar/board_sidebar_milestone_select.vue
'
;
...
...
@@ -18,7 +18,7 @@ export default {
headerHeight
:
`
${
contentTop
()}
px`
,
components
:
{
GlDrawer
,
Issuabl
eTitle
,
BoardSidebarIssu
eTitle
,
BoardSidebarEpicSelect
,
BoardAssigneeDropdown
,
BoardSidebarTimeTracker
,
...
...
@@ -49,11 +49,10 @@ export default {
:header-height=
"$options.headerHeight"
@
close=
"unsetActiveId"
>
<template
#header
>
<issuable-title
:ref-path=
"activeIssue.referencePath"
:title=
"activeIssue.title"
/>
</
template
>
<template
#header
>
{{
__
(
'
Issue details
'
)
}}
</
template
>
<
template
>
<board-sidebar-issue-title
/>
<board-assignee-dropdown
/>
<board-sidebar-epic-select
/>
<board-sidebar-milestone-select
/>
...
...
ee/changelogs/unreleased/232745-edit-issue-title-in-swimlanes-sidebar.yml
0 → 100644
View file @
f4f206b5
---
title
:
Edit issue title in swimlanes sidebar
merge_request
:
46404
author
:
type
:
added
ee/spec/frontend/boards/components/board_content_sidebar_spec.js
View file @
f4f206b5
...
...
@@ -4,7 +4,11 @@ import BoardContentSidebar from 'ee_component/boards/components/board_content_si
import
{
stubComponent
}
from
'
helpers/stub_component
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
BoardAssigneeDropdown
from
'
~/boards/components/board_assignee_dropdown.vue
'
;
import
IssuableTitle
from
'
~/boards/components/issuable_title.vue
'
;
import
BoardSidebarLabelsSelect
from
'
~/boards/components/sidebar/board_sidebar_labels_select.vue
'
;
import
BoardSidebarIssueTitle
from
'
~/boards/components/sidebar/board_sidebar_issue_title.vue
'
;
import
BoardSidebarDueDate
from
'
~/boards/components/sidebar/board_sidebar_due_date.vue
'
;
import
BoardSidebarSubscription
from
'
~/boards/components/sidebar/board_sidebar_subscription.vue
'
;
import
BoardSidebarMilestoneSelect
from
'
~/boards/components/sidebar/board_sidebar_milestone_select.vue
'
;
import
{
ISSUABLE
}
from
'
~/boards/constants
'
;
import
{
createStore
}
from
'
~/boards/stores
'
;
...
...
@@ -16,7 +20,8 @@ describe('ee/BoardContentSidebar', () => {
wrapper
=
shallowMount
(
BoardContentSidebar
,
{
provide
:
{
canUpdate
:
true
,
rootPath
:
''
,
rootPath
:
'
/
'
,
groupId
:
'
#
'
,
},
store
,
stubs
:
{
...
...
@@ -58,14 +63,30 @@ describe('ee/BoardContentSidebar', () => {
expect
(
wrapper
.
find
(
GlDrawer
).
props
(
'
open
'
)).
toBe
(
true
);
});
it
(
'
finds IssuableTitle
'
,
()
=>
{
expect
(
wrapper
.
find
(
IssuableTitle
).
props
(
'
title
'
)).
toContain
(
'
One
'
);
});
it
(
'
renders BoardAssigneeDropdown
'
,
()
=>
{
expect
(
wrapper
.
find
(
BoardAssigneeDropdown
).
exists
()).
toBe
(
true
);
});
it
(
'
renders BoardSidebarLabelsSelect
'
,
()
=>
{
expect
(
wrapper
.
find
(
BoardSidebarLabelsSelect
).
exists
()).
toBe
(
true
);
});
it
(
'
renders BoardSidebarIssueTitle
'
,
()
=>
{
expect
(
wrapper
.
find
(
BoardSidebarIssueTitle
).
exists
()).
toBe
(
true
);
});
it
(
'
renders BoardSidebarDueDate
'
,
()
=>
{
expect
(
wrapper
.
find
(
BoardSidebarDueDate
).
exists
()).
toBe
(
true
);
});
it
(
'
renders BoardSidebarSubscription
'
,
()
=>
{
expect
(
wrapper
.
find
(
BoardSidebarSubscription
).
exists
()).
toBe
(
true
);
});
it
(
'
renders BoardSidebarMilestoneSelect
'
,
()
=>
{
expect
(
wrapper
.
find
(
BoardSidebarMilestoneSelect
).
exists
()).
toBe
(
true
);
});
describe
(
'
when we emit close
'
,
()
=>
{
it
(
'
hides GlDrawer
'
,
async
()
=>
{
expect
(
wrapper
.
find
(
GlDrawer
).
props
(
'
open
'
)).
toBe
(
true
);
...
...
locale/gitlab.pot
View file @
f4f206b5
...
...
@@ -3075,6 +3075,9 @@ msgstr ""
msgid "An error occurred when updating the issue due date"
msgstr ""
msgid "An error occurred when updating the issue title"
msgstr ""
msgid "An error occurred when updating the issue weight"
msgstr ""
...
...
@@ -3381,6 +3384,9 @@ msgstr ""
msgid "An issue can be a bug, a todo or a feature request that needs to be discussed in a project. Besides, issues are searchable and filterable."
msgstr ""
msgid "An issue title is required"
msgstr ""
msgid "An unauthenticated user"
msgstr ""
...
...
@@ -5272,6 +5278,9 @@ msgstr ""
msgid "Changes the title to \"%{title_param}\"."
msgstr ""
msgid "Changes to the title have not been saved"
msgstr ""
msgid "Changes won't take place until the index is %{link_start}recreated%{link_end}."
msgstr ""
...
...
@@ -15567,6 +15576,9 @@ msgstr ""
msgid "Issue created from vulnerability %{vulnerability_link}"
msgstr ""
msgid "Issue details"
msgstr ""
msgid "Issue events"
msgstr ""
...
...
@@ -15582,6 +15594,9 @@ msgstr ""
msgid "Issue published on status page."
msgstr ""
msgid "Issue title"
msgstr ""
msgid "Issue update failed"
msgstr ""
...
...
spec/frontend/boards/components/sidebar/board_editable_item_spec.js
View file @
f4f206b5
...
...
@@ -33,6 +33,14 @@ describe('boards sidebar remove issue', () => {
expect
(
findTitle
().
text
()).
toBe
(
title
);
});
it
(
'
renders provided title slot
'
,
()
=>
{
const
title
=
'
Sidebar item title on slot
'
;
const
slots
=
{
title
:
`<strong>
${
title
}
</strong>`
};
createComponent
({
slots
});
expect
(
wrapper
.
text
()).
toContain
(
title
);
});
it
(
'
hides edit button, loader and expanded content by default
'
,
()
=>
{
createComponent
();
...
...
@@ -74,9 +82,19 @@ describe('boards sidebar remove issue', () => {
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
findCollapsed
().
isVisible
()).
toBe
(
false
);
expect
(
findExpanded
().
isVisible
()).
toBe
(
true
);
expect
(
findExpanded
().
text
()).
toBe
(
'
Select item
'
);
});
});
it
(
'
hides the header while editing if `toggleHeader` is true
'
,
async
()
=>
{
createComponent
({
canUpdate
:
true
,
props
:
{
toggleHeader
:
true
}
});
findEditButton
().
vm
.
$emit
(
'
click
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
findEditButton
().
isVisible
()).
toBe
(
false
);
expect
(
findTitle
().
isVisible
()).
toBe
(
false
);
expect
(
findExpanded
().
isVisible
()).
toBe
(
true
);
});
});
describe
(
'
collapsing an item by offclicking
'
,
()
=>
{
...
...
@@ -96,12 +114,13 @@ describe('boards sidebar remove issue', () => {
expect
(
findExpanded
().
isVisible
()).
toBe
(
false
);
});
it
(
'
emits
close event
'
,
async
()
=>
{
it
(
'
emits
events
'
,
async
()
=>
{
document
.
body
.
click
();
await
wrapper
.
vm
.
$nextTick
();
expect
(
wrapper
.
emitted
().
close
.
length
).
toBe
(
1
);
expect
(
wrapper
.
emitted
().
close
).
toHaveLength
(
1
);
expect
(
wrapper
.
emitted
()[
'
off-click
'
]).
toHaveLength
(
1
);
});
});
...
...
spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js
0 → 100644
View file @
f4f206b5
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlAlert
,
GlFormInput
,
GlForm
}
from
'
@gitlab/ui
'
;
import
BoardSidebarIssueTitle
from
'
~/boards/components/sidebar/board_sidebar_issue_title.vue
'
;
import
BoardEditableItem
from
'
~/boards/components/sidebar/board_editable_item.vue
'
;
import
createFlash
from
'
~/flash
'
;
import
{
createStore
}
from
'
~/boards/stores
'
;
const
TEST_TITLE
=
'
New issue title
'
;
const
TEST_ISSUE_A
=
{
id
:
'
gid://gitlab/Issue/1
'
,
iid
:
8
,
title
:
'
Issue 1
'
,
referencePath
:
'
h/b#1
'
,
};
const
TEST_ISSUE_B
=
{
id
:
'
gid://gitlab/Issue/2
'
,
iid
:
9
,
title
:
'
Issue 2
'
,
referencePath
:
'
h/b#2
'
,
};
jest
.
mock
(
'
~/flash
'
);
describe
(
'
~/boards/components/sidebar/board_sidebar_issue_title.vue
'
,
()
=>
{
let
wrapper
;
let
store
;
afterEach
(()
=>
{
localStorage
.
clear
();
wrapper
.
destroy
();
store
=
null
;
wrapper
=
null
;
});
const
createWrapper
=
(
issue
=
TEST_ISSUE_A
)
=>
{
store
=
createStore
();
store
.
state
.
issues
=
{
[
issue
.
id
]:
{
...
issue
}
};
store
.
dispatch
(
'
setActiveId
'
,
{
id
:
issue
.
id
});
wrapper
=
shallowMount
(
BoardSidebarIssueTitle
,
{
store
,
provide
:
{
canUpdate
:
true
,
},
stubs
:
{
'
board-editable-item
'
:
BoardEditableItem
,
},
});
};
const
findForm
=
()
=>
wrapper
.
find
(
GlForm
);
const
findAlert
=
()
=>
wrapper
.
find
(
GlAlert
);
const
findFormInput
=
()
=>
wrapper
.
find
(
GlFormInput
);
const
findEditableItem
=
()
=>
wrapper
.
find
(
BoardEditableItem
);
const
findCancelButton
=
()
=>
wrapper
.
find
(
'
[data-testid="cancel-button"]
'
);
const
findTitle
=
()
=>
wrapper
.
find
(
'
[data-testid="issue-title"]
'
);
const
findCollapsed
=
()
=>
wrapper
.
find
(
'
[data-testid="collapsed-content"]
'
);
it
(
'
renders title and reference
'
,
()
=>
{
createWrapper
();
expect
(
findTitle
().
text
()).
toContain
(
TEST_ISSUE_A
.
title
);
expect
(
findCollapsed
().
text
()).
toContain
(
TEST_ISSUE_A
.
referencePath
);
});
it
(
'
does not render alert
'
,
()
=>
{
createWrapper
();
expect
(
findAlert
().
exists
()).
toBe
(
false
);
});
describe
(
'
when new title is submitted
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createWrapper
();
jest
.
spyOn
(
wrapper
.
vm
,
'
setActiveIssueTitle
'
).
mockImplementation
(()
=>
{
store
.
state
.
issues
[
TEST_ISSUE_A
.
id
].
title
=
TEST_TITLE
;
});
findFormInput
().
vm
.
$emit
(
'
input
'
,
TEST_TITLE
);
findForm
().
vm
.
$emit
(
'
submit
'
,
{
preventDefault
:
()
=>
{}
});
await
wrapper
.
vm
.
$nextTick
();
});
it
(
'
collapses sidebar and renders new title
'
,
()
=>
{
expect
(
findCollapsed
().
isVisible
()).
toBe
(
true
);
expect
(
findTitle
().
text
()).
toContain
(
TEST_TITLE
);
});
it
(
'
commits change to the server
'
,
()
=>
{
expect
(
wrapper
.
vm
.
setActiveIssueTitle
).
toHaveBeenCalledWith
({
title
:
TEST_TITLE
,
projectPath
:
'
h/b
'
,
});
});
});
describe
(
'
when submitting and invalid title
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createWrapper
();
jest
.
spyOn
(
wrapper
.
vm
,
'
setActiveIssueTitle
'
).
mockImplementation
(()
=>
{});
findFormInput
().
vm
.
$emit
(
'
input
'
,
''
);
findForm
().
vm
.
$emit
(
'
submit
'
,
{
preventDefault
:
()
=>
{}
});
await
wrapper
.
vm
.
$nextTick
();
});
it
(
'
commits change to the server
'
,
()
=>
{
expect
(
wrapper
.
vm
.
setActiveIssueTitle
).
not
.
toHaveBeenCalled
();
});
});
describe
(
'
when abandoning the form without saving
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createWrapper
();
wrapper
.
vm
.
$refs
.
sidebarItem
.
expand
();
findFormInput
().
vm
.
$emit
(
'
input
'
,
TEST_TITLE
);
findEditableItem
().
vm
.
$emit
(
'
off-click
'
);
await
wrapper
.
vm
.
$nextTick
();
});
it
(
'
does not collapses sidebar and shows alert
'
,
()
=>
{
expect
(
findCollapsed
().
isVisible
()).
toBe
(
false
);
expect
(
findAlert
().
exists
()).
toBe
(
true
);
expect
(
localStorage
.
getItem
(
`
${
TEST_ISSUE_A
.
id
}
/issue-title-pending-changes`
)).
toBe
(
TEST_TITLE
,
);
});
});
describe
(
'
when accessing the form with pending changes
'
,
()
=>
{
beforeAll
(()
=>
{
localStorage
.
setItem
(
`
${
TEST_ISSUE_A
.
id
}
/issue-title-pending-changes`
,
TEST_TITLE
);
createWrapper
();
});
it
(
'
sets title, expands item and shows alert
'
,
async
()
=>
{
expect
(
wrapper
.
vm
.
title
).
toBe
(
TEST_TITLE
);
expect
(
findCollapsed
().
isVisible
()).
toBe
(
false
);
expect
(
findAlert
().
exists
()).
toBe
(
true
);
});
});
describe
(
'
when cancel button is clicked
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createWrapper
(
TEST_ISSUE_B
);
jest
.
spyOn
(
wrapper
.
vm
,
'
setActiveIssueTitle
'
).
mockImplementation
(()
=>
{
store
.
state
.
issues
[
TEST_ISSUE_B
.
id
].
title
=
TEST_TITLE
;
});
findFormInput
().
vm
.
$emit
(
'
input
'
,
TEST_TITLE
);
findCancelButton
().
vm
.
$emit
(
'
click
'
);
await
wrapper
.
vm
.
$nextTick
();
});
it
(
'
collapses sidebar and render former title
'
,
()
=>
{
expect
(
wrapper
.
vm
.
setActiveIssueTitle
).
not
.
toHaveBeenCalled
();
expect
(
findCollapsed
().
isVisible
()).
toBe
(
true
);
expect
(
findTitle
().
text
()).
toBe
(
TEST_ISSUE_B
.
title
);
});
});
describe
(
'
when the mutation fails
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createWrapper
(
TEST_ISSUE_B
);
jest
.
spyOn
(
wrapper
.
vm
,
'
setActiveIssueTitle
'
).
mockImplementation
(()
=>
{
throw
new
Error
([
'
failed mutation
'
]);
});
findFormInput
().
vm
.
$emit
(
'
input
'
,
'
Invalid title
'
);
findForm
().
vm
.
$emit
(
'
submit
'
,
{
preventDefault
:
()
=>
{}
});
await
wrapper
.
vm
.
$nextTick
();
});
it
(
'
collapses sidebar and renders former issue title
'
,
()
=>
{
expect
(
findCollapsed
().
isVisible
()).
toBe
(
true
);
expect
(
findTitle
().
text
()).
toContain
(
TEST_ISSUE_B
.
title
);
expect
(
createFlash
).
toHaveBeenCalled
();
});
});
});
spec/frontend/boards/stores/actions_spec.js
View file @
f4f206b5
...
...
@@ -986,6 +986,57 @@ describe('setActiveIssueMilestone', () => {
});
});
describe
(
'
setActiveIssueTitle
'
,
()
=>
{
const
state
=
{
issues
:
{
[
mockIssue
.
id
]:
mockIssue
}
};
const
getters
=
{
activeIssue
:
mockIssue
};
const
testTitle
=
'
Test Title
'
;
const
input
=
{
title
:
testTitle
,
projectPath
:
'
h/b
'
,
};
it
(
'
should commit title after setting the issue
'
,
(
done
)
=>
{
jest
.
spyOn
(
gqlClient
,
'
mutate
'
).
mockResolvedValue
({
data
:
{
updateIssue
:
{
issue
:
{
title
:
testTitle
,
},
errors
:
[],
},
},
});
const
payload
=
{
issueId
:
getters
.
activeIssue
.
id
,
prop
:
'
title
'
,
value
:
testTitle
,
};
testAction
(
actions
.
setActiveIssueTitle
,
input
,
{
...
state
,
...
getters
},
[
{
type
:
types
.
UPDATE_ISSUE_BY_ID
,
payload
,
},
],
[],
done
,
);
});
it
(
'
throws error if fails
'
,
async
()
=>
{
jest
.
spyOn
(
gqlClient
,
'
mutate
'
)
.
mockResolvedValue
({
data
:
{
updateIssue
:
{
errors
:
[
'
failed mutation
'
]
}
}
});
await
expect
(
actions
.
setActiveIssueTitle
({
getters
},
input
)).
rejects
.
toThrow
(
Error
);
});
});
describe
(
'
fetchBacklog
'
,
()
=>
{
expectNotImplemented
(
actions
.
fetchBacklog
);
});
...
...
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