Commit 0e8f4ae8 authored by Simon Knox's avatar Simon Knox

Merge branch 'Give-immediate-feedback-when-emoji-reacting-to-a-comment' into 'master'

Give immediate feedback when awarding an issuable

See merge request gitlab-org/gitlab!75808
parents 8b7b169c 84deccab
...@@ -33,19 +33,50 @@ export const fetchAwards = async ({ commit, dispatch, state }, page = '1') => { ...@@ -33,19 +33,50 @@ export const fetchAwards = async ({ commit, dispatch, state }, page = '1') => {
} }
}; };
/**
* Creates an intermediary award, used for display
* until the real award is loaded from the backend.
*/
const newOptimisticAward = (name, state) => {
const freeId = Math.min(...state.awards.map((a) => a.id), Number.MAX_SAFE_INTEGER) - 1;
return {
id: freeId,
name,
user: {
id: window.gon.current_user_id,
name: window.gon.current_user_fullname,
username: window.gon.current_username,
},
};
};
export const toggleAward = async ({ commit, state }, name) => { export const toggleAward = async ({ commit, state }, name) => {
const award = state.awards.find((a) => a.name === name && a.user.id === state.currentUserId); const award = state.awards.find((a) => a.name === name && a.user.id === state.currentUserId);
try { try {
if (award) { if (award) {
await axios.delete(joinPaths(gon.relative_url_root || '', `${state.path}/${award.id}`));
commit(REMOVE_AWARD, award.id); commit(REMOVE_AWARD, award.id);
await axios
.delete(joinPaths(gon.relative_url_root || '', `${state.path}/${award.id}`))
.catch((err) => {
commit(ADD_NEW_AWARD, award);
throw err;
});
showToast(__('Award removed')); showToast(__('Award removed'));
} else { } else {
const { data } = await axios.post(joinPaths(gon.relative_url_root || '', state.path), { const optimisticAward = newOptimisticAward(name, state);
commit(ADD_NEW_AWARD, optimisticAward);
const { data } = await axios
.post(joinPaths(gon.relative_url_root || '', state.path), {
name, name,
})
.finally(() => {
commit(REMOVE_AWARD, optimisticAward.id);
}); });
commit(ADD_NEW_AWARD, data); commit(ADD_NEW_AWARD, data);
......
...@@ -27,6 +27,7 @@ RSpec.describe 'Merge request > User awards emoji', :js do ...@@ -27,6 +27,7 @@ RSpec.describe 'Merge request > User awards emoji', :js do
it 'removes award from merge request' do it 'removes award from merge request' do
first('[data-testid="award-button"]').click first('[data-testid="award-button"]').click
expect(first('[data-testid="award-button"]')).to have_content '1'
find('[data-testid="award-button"].selected').click find('[data-testid="award-button"].selected').click
expect(first('[data-testid="award-button"]')).to have_content '0' expect(first('[data-testid="award-button"]')).to have_content '0'
......
...@@ -87,6 +87,26 @@ describe('Awards app actions', () => { ...@@ -87,6 +87,26 @@ describe('Awards app actions', () => {
describe('toggleAward', () => { describe('toggleAward', () => {
let mock; let mock;
const optimisticAwardId = Number.MAX_SAFE_INTEGER - 1;
const makeOptimisticAddMutation = (
id = optimisticAwardId,
name = null,
userId = window.gon.current_user_id,
) => ({
type: 'ADD_NEW_AWARD',
payload: {
id,
name,
user: {
id: userId,
},
},
});
const makeOptimisticRemoveMutation = (id = optimisticAwardId) => ({
type: 'REMOVE_AWARD',
payload: id,
});
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
}); });
...@@ -110,8 +130,10 @@ describe('Awards app actions', () => { ...@@ -110,8 +130,10 @@ describe('Awards app actions', () => {
mock.onPost(`${relativeRootUrl || ''}/awards`).reply(200, { id: 1 }); mock.onPost(`${relativeRootUrl || ''}/awards`).reply(200, { id: 1 });
}); });
it('commits ADD_NEW_AWARD', async () => { it('adds an optimistic award, removes it, and then commits ADD_NEW_AWARD', async () => {
testAction(actions.toggleAward, null, { path: '/awards', awards: [] }, [ testAction(actions.toggleAward, null, { path: '/awards', awards: [] }, [
makeOptimisticAddMutation(),
makeOptimisticRemoveMutation(),
{ type: 'ADD_NEW_AWARD', payload: { id: 1 } }, { type: 'ADD_NEW_AWARD', payload: { id: 1 } },
]); ]);
}); });
...@@ -127,7 +149,7 @@ describe('Awards app actions', () => { ...@@ -127,7 +149,7 @@ describe('Awards app actions', () => {
actions.toggleAward, actions.toggleAward,
null, null,
{ path: '/awards', awards: [] }, { path: '/awards', awards: [] },
[], [makeOptimisticAddMutation(), makeOptimisticRemoveMutation()],
[], [],
() => { () => {
expect(Sentry.captureException).toHaveBeenCalled(); expect(Sentry.captureException).toHaveBeenCalled();
...@@ -137,7 +159,7 @@ describe('Awards app actions', () => { ...@@ -137,7 +159,7 @@ describe('Awards app actions', () => {
}); });
}); });
describe('removing a award', () => { describe('removing an award', () => {
const mockData = { id: 1, name: 'thumbsup', user: { id: 1 } }; const mockData = { id: 1, name: 'thumbsup', user: { id: 1 } };
describe('success', () => { describe('success', () => {
...@@ -160,6 +182,9 @@ describe('Awards app actions', () => { ...@@ -160,6 +182,9 @@ describe('Awards app actions', () => {
}); });
describe('error', () => { describe('error', () => {
const currentUserId = 1;
const name = 'thumbsup';
beforeEach(() => { beforeEach(() => {
mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(500); mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(500);
}); });
...@@ -167,13 +192,13 @@ describe('Awards app actions', () => { ...@@ -167,13 +192,13 @@ describe('Awards app actions', () => {
it('calls Sentry.captureException', async () => { it('calls Sentry.captureException', async () => {
await testAction( await testAction(
actions.toggleAward, actions.toggleAward,
'thumbsup', name,
{ {
path: '/awards', path: '/awards',
currentUserId: 1, currentUserId,
awards: [mockData], awards: [mockData],
}, },
[], [makeOptimisticRemoveMutation(1), makeOptimisticAddMutation(1, name, currentUserId)],
[], [],
() => { () => {
expect(Sentry.captureException).toHaveBeenCalled(); expect(Sentry.captureException).toHaveBeenCalled();
......
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