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>
import { __ } from '~/locale';
import { mapGetters } from 'vuex';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
import Icon from '~/vue_shared/components/icon.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 {
name: 'NoteActions',
......@@ -17,6 +21,10 @@ export default {
},
mixins: [resolvedStatusMixin],
props: {
author: {
type: Object,
required: true,
},
authorId: {
type: Number,
required: true,
......@@ -87,7 +95,7 @@ export default {
},
},
computed: {
...mapGetters(['getUserDataByProp']),
...mapGetters(['getUserDataByProp', 'getNoteableData']),
shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
},
......@@ -100,6 +108,26 @@ export default {
currentUserId() {
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: {
onEdit() {
......@@ -116,6 +144,29 @@ export default {
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>
......@@ -215,6 +266,16 @@ export default {
<span class="text-danger">{{ __('Delete comment') }}</span>
</button>
</li>
<li v-if="isIssue">
<button
class="btn-default btn-transparent"
data-testid="assign-user"
type="button"
@click="assignUser"
>
{{ displayAssignUserText }}
</button>
</li>
</ul>
</div>
</div>
......
......@@ -184,6 +184,7 @@ export default {
'updateNote',
'toggleResolveNote',
'scrollToNoteIfNeeded',
'updateAssignees',
]),
editHandler() {
this.isEditing = true;
......@@ -299,6 +300,9 @@ export default {
getLineClasses(lineNumber) {
return getLineClasses(lineNumber);
},
assigneesUpdate(assignees) {
this.updateAssignees(assignees);
},
},
};
</script>
......@@ -355,6 +359,7 @@ export default {
<span v-else-if="note.created_at" class="d-none d-sm-inline">&middot;</span>
</note-header>
<note-actions
:author="author"
:author-id="author.id"
:note-id="note.id"
:note-url="note.noteable_note_url"
......@@ -377,6 +382,7 @@ export default {
@handleDelete="deleteHandler"
@handleResolve="resolveHandler"
@startReplying="$emit('startReplying')"
@updateAssignees="assigneesUpdate"
/>
</div>
<div class="timeline-discussion-body">
......
......@@ -647,5 +647,9 @@ export const receiveDeleteDescriptionVersionError = ({ commit }, 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
export default () => {};
......@@ -23,6 +23,7 @@ export const REMOVE_SUGGESTION_FROM_BATCH = 'REMOVE_SUGGESTION_FROM_BATCH';
export const CLEAR_SUGGESTION_BATCH = 'CLEAR_SUGGESTION_BATCH';
export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION';
export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION';
export const UPDATE_ASSIGNEES = 'UPDATE_ASSIGNEES';
// DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
......
......@@ -355,4 +355,7 @@ export default {
[types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR](state) {
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 ""
msgid "Assign to"
msgstr ""
msgid "Assign to commenting user"
msgstr ""
msgid "Assign yourself to these issues"
msgstr ""
......@@ -20965,6 +20968,9 @@ msgstr ""
msgid "Something went wrong while updating a requirement."
msgstr ""
msgid "Something went wrong while updating assignees"
msgstr ""
msgid "Something went wrong while updating your list settings"
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}"
msgstr ""
msgid "Unassign from commenting user"
msgstr ""
msgid "Unblock"
msgstr ""
......
......@@ -4,26 +4,33 @@ import { TEST_HOST } from 'spec/test_constants';
import createStore from '~/notes/stores';
import noteActions from '~/notes/components/note_actions.vue';
import { userDataMock } from '../mock_data';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
describe('noteActions', () => {
let wrapper;
let store;
let props;
let actions;
let axiosMock;
const shallowMountNoteActions = propsData => {
const shallowMountNoteActions = (propsData, computed) => {
const localVue = createLocalVue();
return shallowMount(localVue.extend(noteActions), {
store,
propsData,
localVue,
computed,
});
};
beforeEach(() => {
store = createStore();
props = {
accessLevel: 'Maintainer',
authorId: 26,
authorId: 1,
author: userDataMock,
canDelete: true,
canEdit: true,
canAwardEmoji: true,
......@@ -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`,
showReply: false,
};
actions = {
updateAssignees: jest.fn(),
};
axiosMock = new AxiosMockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
axiosMock.restore();
});
describe('user is logged in', () => {
......@@ -76,6 +90,14 @@ describe('noteActions', () => {
it('should not show copy link action when `noteUrl` prop is empty', done => {
wrapper.setProps({
...props,
author: {
avatar_url: 'mock_path',
id: 26,
name: 'Example Maintainer',
path: '/ExampleMaintainer',
state: 'active',
username: 'ExampleMaintainer',
},
noteUrl: '',
});
......@@ -104,6 +126,25 @@ describe('noteActions', () => {
})
.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', () => {
});
});
});
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', () => {
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