Updates on success of an MR the count on top and in other tabs
New API endpoint for merge request count Updates all open tabs at the same time with one call Restructured API response API response changed to 401 if no current_user Added API + JS specs Fix for Static Check Updated Count on Open/Close, Assign/Unassign of MR's Checking if MR Count is refreshed Added # frozen_string_literal: true to spec Added Changelog
This commit is contained in:
parent
9d07919465
commit
b9e52612fe
|
@ -24,6 +24,7 @@ const Api = {
|
|||
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
|
||||
projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key',
|
||||
projectTemplatesPath: '/api/:version/projects/:id/templates/:type',
|
||||
userCountsPath: '/api/:version/user_counts',
|
||||
usersPath: '/api/:version/users.json',
|
||||
userPath: '/api/:version/users/:id',
|
||||
userStatusPath: '/api/:version/users/:id/status',
|
||||
|
@ -312,6 +313,11 @@ const Api = {
|
|||
});
|
||||
},
|
||||
|
||||
userCounts() {
|
||||
const url = Api.buildUrl(this.userCountsPath);
|
||||
return axios.get(url);
|
||||
},
|
||||
|
||||
userStatus(id, options) {
|
||||
const url = Api.buildUrl(this.userStatusPath).replace(':id', encodeURIComponent(id));
|
||||
return axios.get(url, {
|
||||
|
|
|
@ -4,3 +4,6 @@ import './jquery';
|
|||
import './bootstrap';
|
||||
import './vue';
|
||||
import '../lib/utils/axios_utils';
|
||||
import { openUserCountsBroadcast } from './nav/user_merge_requests';
|
||||
|
||||
openUserCountsBroadcast();
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import Api from '~/api';
|
||||
|
||||
let channel;
|
||||
|
||||
function broadcastCount(newCount) {
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
channel.postMessage(newCount);
|
||||
}
|
||||
|
||||
function updateUserMergeRequestCounts(newCount) {
|
||||
const mergeRequestsCountEl = document.querySelector('.merge-requests-count');
|
||||
mergeRequestsCountEl.textContent = newCount.toLocaleString();
|
||||
mergeRequestsCountEl.classList.toggle('hidden', Number(newCount) === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh user counts (and broadcast if open)
|
||||
*/
|
||||
export function refreshUserMergeRequestCounts() {
|
||||
return Api.userCounts()
|
||||
.then(({ data }) => {
|
||||
const count = data.merge_requests;
|
||||
|
||||
updateUserMergeRequestCounts(count);
|
||||
broadcastCount(count);
|
||||
})
|
||||
.catch(ex => {
|
||||
console.error(ex); // eslint-disable-line no-console
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the broadcast channel for user counts
|
||||
*/
|
||||
export function closeUserCountsBroadcast() {
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
channel.close();
|
||||
channel = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the broadcast channel for user counts, adds user id so we only update
|
||||
*
|
||||
* **Please note:**
|
||||
* Not supported in all browsers, but not polyfilling for now
|
||||
* to keep bundle size small and
|
||||
* no special functionality lost except cross tab notifications
|
||||
*/
|
||||
export function openUserCountsBroadcast() {
|
||||
closeUserCountsBroadcast();
|
||||
|
||||
if (window.BroadcastChannel) {
|
||||
const currentUserId = typeof gon !== 'undefined' && gon && gon.current_user_id;
|
||||
if (currentUserId) {
|
||||
channel = new BroadcastChannel(`mr_count_channel_${currentUserId}`);
|
||||
channel.onmessage = ev => {
|
||||
updateUserMergeRequestCounts(ev.data);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import {
|
|||
splitCamelCase,
|
||||
slugifyWithUnderscore,
|
||||
} from '../../lib/utils/text_utility';
|
||||
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
|
||||
import * as constants from '../constants';
|
||||
import eventHub from '../event_hub';
|
||||
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
|
||||
|
@ -234,7 +235,10 @@ export default {
|
|||
toggleIssueState() {
|
||||
if (this.isOpen) {
|
||||
this.closeIssue()
|
||||
.then(() => this.enableButton())
|
||||
.then(() => {
|
||||
this.enableButton();
|
||||
refreshUserMergeRequestCounts();
|
||||
})
|
||||
.catch(() => {
|
||||
this.enableButton();
|
||||
this.toggleStateButtonLoading(false);
|
||||
|
@ -247,7 +251,10 @@ export default {
|
|||
});
|
||||
} else {
|
||||
this.reopenIssue()
|
||||
.then(() => this.enableButton())
|
||||
.then(() => {
|
||||
this.enableButton();
|
||||
refreshUserMergeRequestCounts();
|
||||
})
|
||||
.catch(({ data }) => {
|
||||
this.enableButton();
|
||||
this.toggleStateButtonLoading(false);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import Flash from '~/flash';
|
||||
import eventHub from '~/sidebar/event_hub';
|
||||
import Store from '~/sidebar/stores/sidebar_store';
|
||||
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
|
||||
import AssigneeTitle from './assignee_title.vue';
|
||||
import Assignees from './assignees.vue';
|
||||
import { __ } from '~/locale';
|
||||
|
@ -73,6 +74,9 @@ export default {
|
|||
this.mediator
|
||||
.saveAssignees(this.field)
|
||||
.then(setLoadingFalse.bind(this))
|
||||
.then(() => {
|
||||
refreshUserMergeRequestCounts();
|
||||
})
|
||||
.catch(() => {
|
||||
setLoadingFalse();
|
||||
return new Flash(__('Error occurred when saving assignees'));
|
||||
|
|
|
@ -6,6 +6,7 @@ import simplePoll from '~/lib/utils/simple_poll';
|
|||
import { __ } from '~/locale';
|
||||
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
|
||||
import MergeRequest from '../../../merge_request';
|
||||
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
|
||||
import Flash from '../../../flash';
|
||||
import statusIcon from '../mr_widget_status_icon.vue';
|
||||
import eventHub from '../../event_hub';
|
||||
|
@ -174,6 +175,8 @@ export default {
|
|||
MergeRequest.decreaseCounter();
|
||||
stopPolling();
|
||||
|
||||
refreshUserMergeRequestCounts();
|
||||
|
||||
// If user checked remove source branch and we didn't remove the branch yet
|
||||
// we should start another polling for source branch remove process
|
||||
if (this.removeSourceBranch && data.source_branch_exists) {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: New API for User Counts, updates on success of an MR the count on top and in
|
||||
other tabs
|
||||
merge_request: 29441
|
||||
author:
|
||||
type: added
|
|
@ -593,6 +593,30 @@ Example responses
|
|||
}
|
||||
```
|
||||
|
||||
## User counts
|
||||
|
||||
Get the counts (same as in top right menu) of the currently signed in user.
|
||||
|
||||
| Attribute | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `merge_requests` | number | Merge requests that are active and assigned to current user. |
|
||||
|
||||
```
|
||||
GET /user_counts
|
||||
```
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/user_counts"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"merge_requests": 4
|
||||
}
|
||||
```
|
||||
|
||||
## List user projects
|
||||
|
||||
Please refer to the [List of user projects](projects.md#list-user-projects).
|
||||
|
|
|
@ -166,6 +166,7 @@ module API
|
|||
mount ::API::Templates
|
||||
mount ::API::Todos
|
||||
mount ::API::Triggers
|
||||
mount ::API::UserCounts
|
||||
mount ::API::Users
|
||||
mount ::API::Variables
|
||||
mount ::API::Version
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
class UserCounts < Grape::API
|
||||
resource :user_counts do
|
||||
desc 'Return the user specific counts' do
|
||||
detail 'Open MR Count'
|
||||
end
|
||||
get do
|
||||
unauthorized! unless current_user
|
||||
|
||||
{
|
||||
merge_requests: current_user.assigned_open_merge_requests_count
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -412,6 +412,22 @@ describe('Api', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('user counts', () => {
|
||||
it('fetches single user counts', done => {
|
||||
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/user_counts`;
|
||||
mock.onGet(expectedUrl).reply(200, {
|
||||
merge_requests: 4,
|
||||
});
|
||||
|
||||
Api.userCounts()
|
||||
.then(({ data }) => {
|
||||
expect(data.merge_requests).toBe(4);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('user status', () => {
|
||||
it('fetches single user status', done => {
|
||||
const userId = '123456';
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
import {
|
||||
openUserCountsBroadcast,
|
||||
closeUserCountsBroadcast,
|
||||
refreshUserMergeRequestCounts,
|
||||
} from '~/commons/nav/user_merge_requests';
|
||||
import Api from '~/api';
|
||||
|
||||
jest.mock('~/api');
|
||||
|
||||
const TEST_COUNT = 1000;
|
||||
const MR_COUNT_CLASS = 'merge-requests-count';
|
||||
|
||||
describe('User Merge Requests', () => {
|
||||
let channelMock;
|
||||
let newBroadcastChannelMock;
|
||||
|
||||
beforeEach(() => {
|
||||
global.gon.current_user_id = 123;
|
||||
|
||||
channelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
newBroadcastChannelMock = jest.fn().mockImplementation(() => channelMock);
|
||||
|
||||
global.BroadcastChannel = newBroadcastChannelMock;
|
||||
setFixtures(`<div class="${MR_COUNT_CLASS}">0</div>`);
|
||||
});
|
||||
|
||||
const findMRCountText = () => document.body.querySelector(`.${MR_COUNT_CLASS}`).textContent;
|
||||
|
||||
describe('refreshUserMergeRequestCounts', () => {
|
||||
beforeEach(() => {
|
||||
Api.userCounts.mockReturnValue(
|
||||
Promise.resolve({
|
||||
data: { merge_requests: TEST_COUNT },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('with open broadcast channel', () => {
|
||||
beforeEach(() => {
|
||||
openUserCountsBroadcast();
|
||||
|
||||
return refreshUserMergeRequestCounts();
|
||||
});
|
||||
|
||||
it('updates the top count of merge requests', () => {
|
||||
expect(findMRCountText()).toEqual(TEST_COUNT.toLocaleString());
|
||||
});
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(Api.userCounts).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('posts count to BroadcastChannel', () => {
|
||||
expect(channelMock.postMessage).toHaveBeenCalledWith(TEST_COUNT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without open broadcast channel', () => {
|
||||
beforeEach(() => refreshUserMergeRequestCounts());
|
||||
|
||||
it('does not post anything', () => {
|
||||
expect(channelMock.postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('openUserCountsBroadcast', () => {
|
||||
beforeEach(() => {
|
||||
openUserCountsBroadcast();
|
||||
});
|
||||
|
||||
it('creates BroadcastChannel that updates DOM on message received', () => {
|
||||
expect(findMRCountText()).toEqual('0');
|
||||
|
||||
channelMock.onmessage({ data: TEST_COUNT });
|
||||
|
||||
expect(findMRCountText()).toEqual(TEST_COUNT.toLocaleString());
|
||||
});
|
||||
|
||||
it('closes if called while already open', () => {
|
||||
expect(channelMock.close).not.toHaveBeenCalled();
|
||||
|
||||
openUserCountsBroadcast();
|
||||
|
||||
expect(channelMock.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeUserCountsBroadcast', () => {
|
||||
describe('when not opened', () => {
|
||||
it('does nothing', () => {
|
||||
expect(channelMock.close).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when opened', () => {
|
||||
beforeEach(() => {
|
||||
openUserCountsBroadcast();
|
||||
});
|
||||
|
||||
it('closes', () => {
|
||||
expect(channelMock.close).not.toHaveBeenCalled();
|
||||
|
||||
closeUserCountsBroadcast();
|
||||
|
||||
expect(channelMock.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -251,6 +251,21 @@ describe('issue_comment_form component', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when toggling state', () => {
|
||||
it('should update MR count', done => {
|
||||
spyOn(vm, 'closeIssue').and.returnValue(Promise.resolve());
|
||||
|
||||
const updateMrCountSpy = spyOnDependency(CommentForm, 'refreshUserMergeRequestCounts');
|
||||
vm.toggleIssueState();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(updateMrCountSpy).toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('issue is confidential', () => {
|
||||
|
|
|
@ -58,9 +58,11 @@ const createComponent = (customConfig = {}) => {
|
|||
|
||||
describe('ReadyToMerge', () => {
|
||||
let vm;
|
||||
let updateMrCountSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
vm = createComponent();
|
||||
updateMrCountSpy = spyOnDependency(ReadyToMerge, 'refreshUserMergeRequestCounts');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -461,6 +463,7 @@ describe('ReadyToMerge', () => {
|
|||
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('FetchActionsContent');
|
||||
expect(vm.initiateRemoveSourceBranchPolling).toHaveBeenCalled();
|
||||
expect(updateMrCountSpy).toHaveBeenCalled();
|
||||
expect(cpc).toBeFalsy();
|
||||
expect(spc).toBeTruthy();
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe API::UserCounts do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :public) }
|
||||
|
||||
let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, title: "Test") }
|
||||
|
||||
describe 'GET /user_counts' do
|
||||
context 'when unauthenticated' do
|
||||
it 'returns authentication error' do
|
||||
get api('/user_counts')
|
||||
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated' do
|
||||
it 'returns open counts for current user' do
|
||||
get api('/user_counts', user)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response).to be_a Hash
|
||||
expect(json_response['merge_requests']).to eq(1)
|
||||
end
|
||||
|
||||
it 'updates the mr count when a new mr is assigned' do
|
||||
create(:merge_request, source_project: project, author: user, assignees: [user])
|
||||
|
||||
get api('/user_counts', user)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response).to be_a Hash
|
||||
expect(json_response['merge_requests']).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue