Commit ad24d47e authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch...

Merge branch '191455-add-a-button-to-quickly-assign-users-who-have-commented-on-an-issue-or-merge-request' into 'master'

Resolve "Add a button to quickly assign users who have commented on an issue or merge request"

Closes #191455

See merge request gitlab-org/gitlab!23883
parents 1ef53665 a8cb90d6
<script> <script>
import { __ } from '~/locale';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status'; import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import ReplyButton from './note_actions/reply_button.vue'; import ReplyButton from './note_actions/reply_button.vue';
import eventHub from '~/sidebar/event_hub';
import Api from '~/api';
import flash from '~/flash';
export default { export default {
name: 'NoteActions', name: 'NoteActions',
...@@ -17,6 +21,10 @@ export default { ...@@ -17,6 +21,10 @@ export default {
}, },
mixins: [resolvedStatusMixin], mixins: [resolvedStatusMixin],
props: { props: {
author: {
type: Object,
required: true,
},
authorId: { authorId: {
type: Number, type: Number,
required: true, required: true,
...@@ -87,7 +95,7 @@ export default { ...@@ -87,7 +95,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters(['getUserDataByProp']), ...mapGetters(['getUserDataByProp', 'getNoteableData']),
shouldShowActionsDropdown() { shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse); return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
}, },
...@@ -100,6 +108,26 @@ export default { ...@@ -100,6 +108,26 @@ export default {
currentUserId() { currentUserId() {
return this.getUserDataByProp('id'); return this.getUserDataByProp('id');
}, },
isUserAssigned() {
return this.assignees && this.assignees.some(({ id }) => id === this.author.id);
},
displayAssignUserText() {
return this.isUserAssigned
? __('Unassign from commenting user')
: __('Assign to commenting user');
},
sidebarAction() {
return this.isUserAssigned ? 'sidebar.addAssignee' : 'sidebar.removeAssignee';
},
targetType() {
return this.getNoteableData.targetType;
},
assignees() {
return this.getNoteableData.assignees || [];
},
isIssue() {
return this.targetType === 'issue';
},
}, },
methods: { methods: {
onEdit() { onEdit() {
...@@ -116,6 +144,29 @@ export default { ...@@ -116,6 +144,29 @@ export default {
this.$root.$emit('bv::hide::tooltip'); this.$root.$emit('bv::hide::tooltip');
}); });
}, },
handleAssigneeUpdate(assignees) {
this.$emit('updateAssignees', assignees);
eventHub.$emit(this.sidebarAction, this.author);
eventHub.$emit('sidebar.saveAssignees');
},
assignUser() {
let { assignees } = this;
const { project_id, iid } = this.getNoteableData;
if (this.isUserAssigned) {
assignees = assignees.filter(assignee => assignee.id !== this.author.id);
} else {
assignees.push({ id: this.author.id });
}
if (this.targetType === 'issue') {
Api.updateIssue(project_id, iid, {
assignee_ids: assignees.map(assignee => assignee.id),
})
.then(() => this.handleAssigneeUpdate(assignees))
.catch(() => flash(__('Something went wrong while updating assignees')));
}
},
}, },
}; };
</script> </script>
...@@ -215,6 +266,16 @@ export default { ...@@ -215,6 +266,16 @@ export default {
<span class="text-danger">{{ __('Delete comment') }}</span> <span class="text-danger">{{ __('Delete comment') }}</span>
</button> </button>
</li> </li>
<li v-if="isIssue">
<button
class="btn-default btn-transparent"
data-testid="assign-user"
type="button"
@click="assignUser"
>
{{ displayAssignUserText }}
</button>
</li>
</ul> </ul>
</div> </div>
</div> </div>
......
...@@ -184,6 +184,7 @@ export default { ...@@ -184,6 +184,7 @@ export default {
'updateNote', 'updateNote',
'toggleResolveNote', 'toggleResolveNote',
'scrollToNoteIfNeeded', 'scrollToNoteIfNeeded',
'updateAssignees',
]), ]),
editHandler() { editHandler() {
this.isEditing = true; this.isEditing = true;
...@@ -299,6 +300,9 @@ export default { ...@@ -299,6 +300,9 @@ export default {
getLineClasses(lineNumber) { getLineClasses(lineNumber) {
return getLineClasses(lineNumber); return getLineClasses(lineNumber);
}, },
assigneesUpdate(assignees) {
this.updateAssignees(assignees);
},
}, },
}; };
</script> </script>
...@@ -355,6 +359,7 @@ export default { ...@@ -355,6 +359,7 @@ export default {
<span v-else-if="note.created_at" class="d-none d-sm-inline">&middot;</span> <span v-else-if="note.created_at" class="d-none d-sm-inline">&middot;</span>
</note-header> </note-header>
<note-actions <note-actions
:author="author"
:author-id="author.id" :author-id="author.id"
:note-id="note.id" :note-id="note.id"
:note-url="note.noteable_note_url" :note-url="note.noteable_note_url"
...@@ -377,6 +382,7 @@ export default { ...@@ -377,6 +382,7 @@ export default {
@handleDelete="deleteHandler" @handleDelete="deleteHandler"
@handleResolve="resolveHandler" @handleResolve="resolveHandler"
@startReplying="$emit('startReplying')" @startReplying="$emit('startReplying')"
@updateAssignees="assigneesUpdate"
/> />
</div> </div>
<div class="timeline-discussion-body"> <div class="timeline-discussion-body">
......
...@@ -647,5 +647,9 @@ export const receiveDeleteDescriptionVersionError = ({ commit }, error) => { ...@@ -647,5 +647,9 @@ export const receiveDeleteDescriptionVersionError = ({ commit }, error) => {
commit(types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR, error); commit(types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR, error);
}; };
export const updateAssignees = ({ commit }, assignees) => {
commit(types.UPDATE_ASSIGNEES, assignees);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -23,6 +23,7 @@ export const REMOVE_SUGGESTION_FROM_BATCH = 'REMOVE_SUGGESTION_FROM_BATCH'; ...@@ -23,6 +23,7 @@ export const REMOVE_SUGGESTION_FROM_BATCH = 'REMOVE_SUGGESTION_FROM_BATCH';
export const CLEAR_SUGGESTION_BATCH = 'CLEAR_SUGGESTION_BATCH'; export const CLEAR_SUGGESTION_BATCH = 'CLEAR_SUGGESTION_BATCH';
export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION'; export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION';
export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION'; export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION';
export const UPDATE_ASSIGNEES = 'UPDATE_ASSIGNEES';
// DISCUSSION // DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION'; export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
......
...@@ -355,4 +355,7 @@ export default { ...@@ -355,4 +355,7 @@ export default {
[types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR](state) { [types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR](state) {
state.isLoadingDescriptionVersion = false; state.isLoadingDescriptionVersion = false;
}, },
[types.UPDATE_ASSIGNEES](state, assignees) {
state.noteableData.assignees = assignees;
},
}; };
---
title: Resolve Add a button to assign users who have commented on an issue
merge_request: 23883
author:
type: added
...@@ -2970,6 +2970,9 @@ msgstr "" ...@@ -2970,6 +2970,9 @@ msgstr ""
msgid "Assign to" msgid "Assign to"
msgstr "" msgstr ""
msgid "Assign to commenting user"
msgstr ""
msgid "Assign yourself to these issues" msgid "Assign yourself to these issues"
msgstr "" msgstr ""
...@@ -20965,6 +20968,9 @@ msgstr "" ...@@ -20965,6 +20968,9 @@ msgstr ""
msgid "Something went wrong while updating a requirement." msgid "Something went wrong while updating a requirement."
msgstr "" msgstr ""
msgid "Something went wrong while updating assignees"
msgstr ""
msgid "Something went wrong while updating your list settings" msgid "Something went wrong while updating your list settings"
msgstr "" msgstr ""
...@@ -24074,6 +24080,9 @@ msgstr "" ...@@ -24074,6 +24080,9 @@ msgstr ""
msgid "Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}" msgid "Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}"
msgstr "" msgstr ""
msgid "Unassign from commenting user"
msgstr ""
msgid "Unblock" msgid "Unblock"
msgstr "" msgstr ""
......
...@@ -4,26 +4,33 @@ import { TEST_HOST } from 'spec/test_constants'; ...@@ -4,26 +4,33 @@ import { TEST_HOST } from 'spec/test_constants';
import createStore from '~/notes/stores'; import createStore from '~/notes/stores';
import noteActions from '~/notes/components/note_actions.vue'; import noteActions from '~/notes/components/note_actions.vue';
import { userDataMock } from '../mock_data'; import { userDataMock } from '../mock_data';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
describe('noteActions', () => { describe('noteActions', () => {
let wrapper; let wrapper;
let store; let store;
let props; let props;
let actions;
let axiosMock;
const shallowMountNoteActions = propsData => { const shallowMountNoteActions = (propsData, computed) => {
const localVue = createLocalVue(); const localVue = createLocalVue();
return shallowMount(localVue.extend(noteActions), { return shallowMount(localVue.extend(noteActions), {
store, store,
propsData, propsData,
localVue, localVue,
computed,
}); });
}; };
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore();
props = { props = {
accessLevel: 'Maintainer', accessLevel: 'Maintainer',
authorId: 26, authorId: 1,
author: userDataMock,
canDelete: true, canDelete: true,
canEdit: true, canEdit: true,
canAwardEmoji: true, canAwardEmoji: true,
...@@ -33,10 +40,17 @@ describe('noteActions', () => { ...@@ -33,10 +40,17 @@ describe('noteActions', () => {
reportAbusePath: `${TEST_HOST}/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26`, reportAbusePath: `${TEST_HOST}/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26`,
showReply: false, showReply: false,
}; };
actions = {
updateAssignees: jest.fn(),
};
axiosMock = new AxiosMockAdapter(axios);
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
axiosMock.restore();
}); });
describe('user is logged in', () => { describe('user is logged in', () => {
...@@ -76,6 +90,14 @@ describe('noteActions', () => { ...@@ -76,6 +90,14 @@ describe('noteActions', () => {
it('should not show copy link action when `noteUrl` prop is empty', done => { it('should not show copy link action when `noteUrl` prop is empty', done => {
wrapper.setProps({ wrapper.setProps({
...props, ...props,
author: {
avatar_url: 'mock_path',
id: 26,
name: 'Example Maintainer',
path: '/ExampleMaintainer',
state: 'active',
username: 'ExampleMaintainer',
},
noteUrl: '', noteUrl: '',
}); });
...@@ -104,6 +126,25 @@ describe('noteActions', () => { ...@@ -104,6 +126,25 @@ describe('noteActions', () => {
}) })
.catch(done.fail); .catch(done.fail);
}); });
it('should be possible to assign or unassign the comment author', () => {
wrapper = shallowMountNoteActions(props, {
targetType: () => 'issue',
});
const assignUserButton = wrapper.find('[data-testid="assign-user"]');
expect(assignUserButton.exists()).toBe(true);
assignUserButton.trigger('click');
axiosMock.onPut(`${TEST_HOST}/api/v4/projects/group/project/issues/1`).reply(() => {
expect(actions.updateAssignees).toHaveBeenCalled();
});
});
it('should not be possible to assign or unassign the comment author in a merge request', () => {
const assignUserButton = wrapper.find('[data-testid="assign-user"]');
expect(assignUserButton.exists()).toBe(false);
});
}); });
}); });
......
...@@ -1141,4 +1141,17 @@ describe('Actions Notes Store', () => { ...@@ -1141,4 +1141,17 @@ describe('Actions Notes Store', () => {
}); });
}); });
}); });
describe('updateAssignees', () => {
it('update the assignees state', done => {
testAction(
actions.updateAssignees,
[userDataMock.id],
{ state: noteableDataMock },
[{ type: mutationTypes.UPDATE_ASSIGNEES, payload: [userDataMock.id] }],
[],
done,
);
});
});
}); });
...@@ -805,4 +805,16 @@ describe('Notes Store mutations', () => { ...@@ -805,4 +805,16 @@ describe('Notes Store mutations', () => {
expect(state.batchSuggestionsInfo.length).toEqual(0); expect(state.batchSuggestionsInfo.length).toEqual(0);
}); });
}); });
describe('UPDATE_ASSIGNEES', () => {
it('should update assignees', () => {
const state = {
noteableData: noteableDataMock,
};
mutations.UPDATE_ASSIGNEES(state, [userDataMock.id]);
expect(state.noteableData.assignees).toEqual([userDataMock.id]);
});
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment