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
6c174bee
Commit
6c174bee
authored
Dec 22, 2021
by
Kushal Pandya
Committed by
Natalia Tepluhina
Dec 22, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add support for bulk editing epics
parent
f52ac300
Changes
8
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
306 additions
and
7 deletions
+306
-7
app/assets/javascripts/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue
...d/issuable/list/components/issuable_bulk_edit_sidebar.vue
+1
-0
ee/app/assets/javascripts/epics_list/components/epics_list_bulk_edit_sidebar.vue
...ts/epics_list/components/epics_list_bulk_edit_sidebar.vue
+93
-0
ee/app/assets/javascripts/epics_list/components/epics_list_root.vue
...ets/javascripts/epics_list/components/epics_list_root.vue
+55
-2
ee/app/assets/javascripts/epics_list/epics_list_bundle.js
ee/app/assets/javascripts/epics_list/epics_list_bundle.js
+4
-2
ee/app/views/groups/epics/index.html.haml
ee/app/views/groups/epics/index.html.haml
+2
-1
ee/spec/features/epics/epics_list_spec.rb
ee/spec/features/epics/epics_list_spec.rb
+52
-2
ee/spec/frontend/epics_list/components/epics_list_bulk_edit_sidebar_spec.js
...pics_list/components/epics_list_bulk_edit_sidebar_spec.js
+93
-0
locale/gitlab.pot
locale/gitlab.pot
+6
-0
No files found.
app/assets/javascripts/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue
View file @
6c174bee
...
...
@@ -13,6 +13,7 @@ export default {
if
(
layoutPageEl
)
{
layoutPageEl
.
classList
.
toggle
(
'
right-sidebar-expanded
'
,
value
);
layoutPageEl
.
classList
.
toggle
(
'
right-sidebar-collapsed
'
,
!
value
);
layoutPageEl
.
classList
.
toggle
(
'
issuable-bulk-update-sidebar
'
,
!
value
);
}
},
},
...
...
ee/app/assets/javascripts/epics_list/components/epics_list_bulk_edit_sidebar.vue
0 → 100644
View file @
6c174bee
<
script
>
import
{
GlForm
,
GlFormGroup
}
from
'
@gitlab/ui
'
;
import
{
uniqBy
}
from
'
lodash
'
;
import
LabelsSelectWidget
from
'
~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
'
;
import
{
DropdownVariant
}
from
'
~/vue_shared/components/sidebar/labels_select_vue/constants
'
;
import
csrf
from
'
~/lib/utils/csrf
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
export
default
{
csrf
,
DropdownVariant
,
getIdFromGraphQLId
,
components
:
{
GlForm
,
GlFormGroup
,
LabelsSelectWidget
,
},
inject
:
[
'
labelsManagePath
'
,
'
labelsFetchPath
'
],
props
:
{
checkedEpics
:
{
type
:
Array
,
required
:
true
,
},
},
data
()
{
return
{
selectedLabelIds
:
[],
removedLabelIds
:
[],
};
},
computed
:
{
/**
* This prop returns a unique list of labels
* applied on all the selected epics while
* also making sure that `id` is numeri
* instead of GraphQL ID string.
*/
existingSelectedLabels
()
{
if
(
!
this
.
checkedEpics
.
length
)
{
return
[];
}
return
uniqBy
(
this
.
checkedEpics
.
reduce
((
labels
,
epic
)
=>
{
if
(
epic
.
labels
.
nodes
.
length
)
{
const
labelsForEpic
=
epic
.
labels
.
nodes
.
map
((
label
)
=>
({
...
label
,
id
:
getIdFromGraphQLId
(
label
.
id
),
}));
labels
.
push
(...
labelsForEpic
);
}
return
labels
;
},
[]),
'
id
'
,
);
},
},
methods
:
{
handleSelectedLabels
(
labels
)
{
this
.
selectedLabelIds
=
[...
labels
].
map
((
label
)
=>
label
.
id
);
this
.
removedLabelIds
=
this
.
existingSelectedLabels
.
filter
((
label
)
=>
!
this
.
selectedLabelIds
.
includes
(
label
.
id
))
.
map
((
label
)
=>
label
.
id
);
},
handleFormSubmitted
()
{
const
bulkUpdateData
=
{
issuable_ids
:
this
.
checkedEpics
.
map
((
epic
)
=>
getIdFromGraphQLId
(
epic
.
id
)).
join
(
'
,
'
),
add_label_ids
:
this
.
selectedLabelIds
,
remove_label_ids
:
this
.
removedLabelIds
,
};
this
.
$emit
(
'
bulk-update
'
,
bulkUpdateData
);
},
},
};
</
script
>
<
template
>
<gl-form
id=
"epics-list-bulk-edit"
@
submit.prevent=
"handleFormSubmitted"
>
<gl-form-group
:label=
"__('Labels')"
class=
"block gl-p-0! gl-m-auto gl-mt-6"
>
<labels-select-widget
:allow-label-edit=
"true"
:allow-multiselect=
"true"
:allow-scoped-labels=
"true"
:selected-labels=
"existingSelectedLabels"
:labels-fetch-path=
"labelsFetchPath"
:labels-manage-path=
"labelsManagePath"
:variant=
"$options.DropdownVariant.Embedded"
@
updateSelectedLabels=
"handleSelectedLabels"
/>
</gl-form-group>
</gl-form>
</
template
>
ee/app/assets/javascripts/epics_list/components/epics_list_root.vue
View file @
6c174bee
<
script
>
import
{
GlButton
,
GlIcon
}
from
'
@gitlab/ui
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
EpicsFilteredSearchMixin
from
'
ee/roadmap/mixins/filtered_search_mixin
'
;
...
...
@@ -15,6 +16,7 @@ import { EpicsSortOptions } from '../constants';
import
groupEpics
from
'
../queries/group_epics.query.graphql
'
;
import
EpicsListEmptyState
from
'
./epics_list_empty_state.vue
'
;
import
EpicsListBulkEditSidebar
from
'
./epics_list_bulk_edit_sidebar.vue
'
;
export
default
{
IssuableListTabs
,
...
...
@@ -26,6 +28,7 @@ export default {
GlIcon
,
IssuableList
,
EpicsListEmptyState
,
EpicsListBulkEditSidebar
,
},
mixins
:
[
EpicsFilteredSearchMixin
],
inject
:
[
...
...
@@ -39,7 +42,7 @@ export default {
'
epicsCount
'
,
'
epicNewPath
'
,
'
groupFullPath
'
,
'
groupLabel
sPath
'
,
'
listEpic
sPath
'
,
'
groupMilestonesPath
'
,
'
emptyStatePath
'
,
'
isSignedIn
'
,
...
...
@@ -108,6 +111,8 @@ export default {
nextPageCursor
:
this
.
next
,
filterParams
:
this
.
initialFilterParams
,
sortedBy
:
this
.
initialSortBy
,
showBulkEditSidebar
:
false
,
bulkEditInProgress
:
false
,
epics
:
{
list
:
[],
pageInfo
:
{},
...
...
@@ -198,6 +203,26 @@ export default {
handleFilterEpics
(
filters
)
{
this
.
filterParams
=
this
.
getFilterParams
(
filters
);
},
/**
* Bulk editing Issuables (or Epics in this case) is not supported
* via GraphQL mutations, so we're using legacy API to do it,
* hence we're making a POST call within the component.
*/
handleEpicsBulkUpdate
(
update
)
{
this
.
bulkEditInProgress
=
true
;
axios
.
post
(
`
${
this
.
listEpicsPath
}
/bulk_update`
,
{
update
,
})
.
then
(()
=>
window
.
location
.
reload
())
.
catch
((
error
)
=>
{
createFlash
({
message
:
s__
(
'
Epics|Something went wrong while updating epics.
'
),
captureError
:
true
,
error
,
});
});
},
},
};
</
script
>
...
...
@@ -215,6 +240,7 @@ export default {
:initial-sort-by="sortedBy"
:issuables="epics.list"
:issuables-loading="epicsListLoading"
:show-bulk-edit-sidebar="showBulkEditSidebar"
:show-pagination-controls="showPaginationControls"
:show-discussions="true"
:default-page-size="$options.defaultPageSize"
...
...
@@ -230,10 +256,37 @@ export default {
@filter="handleFilterEpics"
>
<template
v-if=
"canCreateEpic || canBulkEditEpics"
#nav-actions
>
<gl-button
v-if=
"canCreateEpic"
category=
"primary"
variant=
"success"
:href=
"epicNewPath"
>
{{
<gl-button
v-if=
"canBulkEditEpics"
:disabled=
"showBulkEditSidebar"
@
click=
"showBulkEditSidebar = true"
>
{{
__
(
'
Edit epics
'
)
}}
</gl-button
>
<gl-button
v-if=
"canCreateEpic"
category=
"primary"
variant=
"confirm"
:href=
"epicNewPath"
>
{{
__
(
'
New epic
'
)
}}
</gl-button>
</
template
>
<
template
#bulk-edit-actions=
"{ checkedIssuables }"
>
<gl-button
category=
"primary"
variant=
"confirm"
type=
"submit"
class=
"js-update-selected-issues"
form=
"epics-list-bulk-edit"
:disabled=
"checkedIssuables.length === 0 || bulkEditInProgress"
:loading=
"bulkEditInProgress"
>
{{
__
(
'
Update all
'
)
}}
</gl-button
>
<gl-button
class=
"gl-float-right"
@
click=
"showBulkEditSidebar = false"
>
{{
__
(
'
Cancel
'
)
}}
</gl-button>
</
template
>
<
template
#sidebar-items=
"{ checkedIssuables }"
>
<epics-list-bulk-edit-sidebar
:checked-epics=
"checkedIssuables"
@
bulk-update=
"handleEpicsBulkUpdate"
/>
</
template
>
<
template
#reference=
"{ issuable }"
>
<span
class=
"issuable-reference"
>
{{
epicReference
(
issuable
)
}}
</span>
</
template
>
...
...
ee/app/assets/javascripts/epics_list/epics_list_bundle.js
View file @
6c174bee
...
...
@@ -35,7 +35,8 @@ export default function initEpicsList({ mountPointSelector }) {
epicNewPath
,
listEpicsPath
,
groupFullPath
,
groupLabelsPath
,
labelsManagePath
,
labelsFetchPath
,
groupMilestonesPath
,
emptyStatePath
,
isSignedIn
,
...
...
@@ -69,10 +70,11 @@ export default function initEpicsList({ mountPointSelector }) {
[
IssuableStates
.
Closed
]:
parseInt
(
epicsCountClosed
,
10
),
[
IssuableStates
.
All
]:
parseInt
(
epicsCountAll
,
10
),
},
labelsFetchPath
:
`
${
labelsFetchPath
}
?only_group_labels=true`
,
epicNewPath
,
listEpicsPath
,
groupFullPath
,
groupLabels
Path
,
labelsManage
Path
,
groupMilestonesPath
,
emptyStatePath
,
isSignedIn
:
parseBoolean
(
isSignedIn
),
...
...
ee/app/views/groups/epics/index.html.haml
View file @
6c174bee
...
...
@@ -17,7 +17,8 @@
epic_new_path:
new_group_epic_url
(
@group
),
list_epics_path:
group_epics_path
(
@group
),
group_full_path:
@group
.
full_path
,
group_labels_path:
group_labels_path
(
@group
,
format: :json
),
labels_manage_path:
group_labels_path
(
@group
),
labels_fetch_path:
group_labels_path
(
@group
,
format: :json
),
group_milestones_path:
group_milestones_path
(
@group
,
format: :json
),
empty_state_path:
image_path
(
'illustrations/epics/list.svg'
),
is_signed_in:
is_signed_in
}
}
...
...
ee/spec/features/epics/epics_list_spec.rb
View file @
6c174bee
...
...
@@ -227,8 +227,9 @@ RSpec.describe 'epics list', :js do
wait_for_requests
end
it
'renders New Epic Link'
do
page
.
within
(
'.issuable-list-container'
)
do
it
'renders epics list header actions'
,
:aggregate_failures
do
page
.
within
(
'.issuable-list-container .nav-controls'
)
do
expect
(
page
).
to
have_button
(
'Edit epics'
)
expect
(
page
).
to
have_link
(
'New epic'
)
end
end
...
...
@@ -236,6 +237,55 @@ RSpec.describe 'epics list', :js do
it_behaves_like
'epic list'
it_behaves_like
'filtered search bar'
,
available_tokens
it
'shows bulk editing sidebar with actions and labels select dropdown'
,
:aggregate_failures
do
click_button
'Edit epics'
page
.
within
(
'.issuable-list-container aside.right-sidebar'
)
do
expect
(
page
).
to
have_button
(
'Update all'
,
disabled:
true
)
expect
(
page
).
to
have_button
(
'Cancel'
)
expect
(
page
).
to
have_selector
(
'form#epics-list-bulk-edit'
)
expect
(
page
).
to
have_button
(
'Label'
)
end
end
it
'shows checkboxes for selecting epics while bulk editing sidebar is visible'
,
:aggregate_failures
do
click_button
'Edit epics'
page
.
within
(
'.issuable-list-container'
)
do
expect
(
page
).
to
have_selector
(
'.vue-filtered-search-bar-container input[type="checkbox"]'
)
expect
(
page
.
first
(
'.issuable-list li.issue'
)).
to
have_selector
(
'.gl-form-checkbox input[type="checkbox"]'
)
end
end
it
'applies label to multiple epics from bulk editing sidebar'
,
:aggregate_failures
do
# Vertify that no labels are applied already
expect
(
find
(
'.issuable-list li.issue .issuable-info'
,
match: :first
)).
not_to
have_selector
(
'.gl-label'
)
# Bulk edit all epics to apply label
page
.
within
(
'.issuable-list-container'
)
do
click_button
'Edit epics'
page
.
within
(
'.vue-filtered-search-bar-container'
)
do
page
.
find
(
'input[type="checkbox"]'
).
click
end
page
.
within
(
'aside.right-sidebar'
)
do
click_button
'Label'
wait_for_requests
click_link
bug_label
.
title
click_button
'Update all'
wait_for_requests
end
end
# Verify that label is applied
expect
(
find
(
'.issuable-list li.issue .issuable-info'
,
match: :first
)).
to
have_selector
(
'.gl-label'
,
text:
bug_label
.
title
)
end
end
context
'when signed out'
do
...
...
ee/spec/frontend/epics_list/components/epics_list_bulk_edit_sidebar_spec.js
0 → 100644
View file @
6c174bee
import
{
GlForm
,
GlFormGroup
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
EpicsListBulkEditSidebar
from
'
ee/epics_list/components/epics_list_bulk_edit_sidebar.vue
'
;
import
{
mockFormattedEpic
,
mockFormattedEpic2
}
from
'
ee_jest/roadmap/mock_data
'
;
import
{
mockLabels
,
mockRegularLabel
,
mockScopedLabel
,
}
from
'
jest/vue_shared/components/sidebar/labels_select_vue/mock_data
'
;
import
LabelsSelectWidget
from
'
~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
'
;
const
mockEpic1
=
{
...
mockFormattedEpic
,
id
:
'
gid://gitlab/Epic/1
'
,
labels
:
{
nodes
:
[
mockRegularLabel
],
},
};
const
mockEpic2
=
{
...
mockFormattedEpic2
,
id
:
'
gid://gitlab/Epic/2
'
,
labels
:
{
nodes
:
[
mockScopedLabel
],
},
};
const
labelsFetchPath
=
'
/gitlab-org/my-project/-/labels.json
'
;
const
labelsManagePath
=
'
/gitlab-org/my-project/-/labels
'
;
const
createComponent
=
({
checkedEpics
=
[
mockEpic1
,
mockEpic2
]
}
=
{})
=>
shallowMount
(
EpicsListBulkEditSidebar
,
{
propsData
:
{
checkedEpics
,
},
provide
:
{
labelsFetchPath
,
labelsManagePath
,
},
});
describe
(
'
EpicsListBulkEditSidebar
'
,
()
=>
{
let
wrapper
;
const
findLabelsSelect
=
()
=>
wrapper
.
findComponent
(
LabelsSelectWidget
);
beforeEach
(()
=>
{
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
(
'
renders gl-form with labels-select-widget
'
,
()
=>
{
expect
(
wrapper
.
findComponent
(
GlForm
).
attributes
(
'
id
'
)).
toBe
(
'
epics-list-bulk-edit
'
);
expect
(
wrapper
.
findComponent
(
GlFormGroup
).
attributes
(
'
label
'
)).
toBe
(
'
Labels
'
);
expect
(
findLabelsSelect
().
exists
()).
toBe
(
true
);
expect
(
findLabelsSelect
().
props
()).
toMatchObject
({
allowLabelEdit
:
true
,
allowMultiselect
:
true
,
allowScopedLabels
:
true
,
selectedLabels
:
[
mockRegularLabel
,
mockScopedLabel
],
labelsFetchPath
,
labelsManagePath
,
variant
:
'
embedded
'
,
});
});
it
(
'
emits `bulk-update` event with request payload object on component after labels are selected/unselected
'
,
async
()
=>
{
// We're slicing `mockLabels` as it already includes
// 2 labels (as last 2 elements) that epics have present.
findLabelsSelect
().
vm
.
$emit
(
'
updateSelectedLabels
'
,
mockLabels
.
slice
(
0
,
2
));
await
wrapper
.
vm
.
$nextTick
();
wrapper
.
findComponent
(
GlForm
).
vm
.
$emit
(
'
submit
'
,
{
preventDefault
:
jest
.
fn
(),
});
await
wrapper
.
vm
.
$nextTick
();
expect
(
wrapper
.
emitted
(
'
bulk-update
'
)).
toBeDefined
();
expect
(
wrapper
.
emitted
(
'
bulk-update
'
)[
0
]).
toEqual
([
{
issuable_ids
:
'
1,2
'
,
add_label_ids
:
[
29
,
28
],
remove_label_ids
:
[
26
,
27
],
},
]);
});
});
locale/gitlab.pot
View file @
6c174bee
...
...
@@ -12832,6 +12832,9 @@ msgstr ""
msgid "Edit environment"
msgstr ""
msgid "Edit epics"
msgstr ""
msgid "Edit files in the editor and commit changes here"
msgstr ""
...
...
@@ -13773,6 +13776,9 @@ msgstr ""
msgid "Epics|Something went wrong while removing issue from epic."
msgstr ""
msgid "Epics|Something went wrong while updating epics."
msgstr ""
msgid "Epics|This epic and any containing child epics are confidential and should only be visible to team members with at least Reporter access."
msgstr ""
...
...
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