summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Eastwood <contact@ericeastwood.com>2017-11-14 00:05:40 -0600
committerEric Eastwood <contact@ericeastwood.com>2017-11-16 16:52:49 -0600
commit7206a3c53eea5b644eac91da5da003ff7ec5ce44 (patch)
tree2559e423279e17f8813d2d33e699b80417c4d709
parent92b8ee5712ff668c9f188492db657061f930e406 (diff)
downloadgitlab-ce-39167-async-boards-sidebar.tar.gz
Async notification subscriptions in issue boards39167-async-boards-sidebar
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js52
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue (renamed from app/assets/javascripts/boards/components/board_card.js)40
-rw-r--r--app/assets/javascripts/boards/components/board_list.js2
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js11
-rw-r--r--app/assets/javascripts/boards/models/issue.js13
-rw-r--r--app/assets/javascripts/boards/models/list.js1
-rw-r--r--app/assets/javascripts/boards/services/board_service.js10
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js1
-rw-r--r--app/assets/javascripts/main.js1
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue6
-rw-r--r--app/assets/javascripts/subscription.js45
-rw-r--r--app/views/shared/boards/components/sidebar/_notifications.html.haml10
-rw-r--r--changelogs/unreleased/39167-async-boards-sidebar.yml5
-rw-r--r--spec/features/boards/sidebar_spec.rb22
-rw-r--r--spec/javascripts/boards/board_card_spec.js29
-rw-r--r--spec/javascripts/boards/issue_spec.js13
-rw-r--r--spec/javascripts/vue_shared/components/loading_button_spec.js1
18 files changed, 166 insertions, 99 deletions
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index ef4093b59e3..20d23162940 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -1,12 +1,13 @@
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
-/* global BoardService */
import _ from 'underscore';
import Vue from 'vue';
import VueResource from 'vue-resource';
import Flash from '../flash';
+import { __ } from '../locale';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
+import sidebarEventHub from '../sidebar/event_hub';
import './models/issue';
import './models/label';
import './models/list';
@@ -14,7 +15,7 @@ import './models/milestone';
import './models/assignee';
import './stores/boards_store';
import './stores/modal_store';
-import './services/board_service';
+import BoardService from './services/board_service';
import './mixins/modal_mixins';
import './mixins/sortable_default_options';
import './filters/due_date_filters';
@@ -77,11 +78,16 @@ $(() => {
});
Store.rootPath = this.boardsEndpoint;
- // Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens);
+ eventHub.$on('newDetailIssue', this.updateDetailIssue);
+ eventHub.$on('clearDetailIssue', this.clearDetailIssue);
+ sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
},
beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens);
+ eventHub.$off('newDetailIssue', this.updateDetailIssue);
+ eventHub.$off('clearDetailIssue', this.clearDetailIssue);
+ sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
},
mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true);
@@ -112,6 +118,46 @@ $(() => {
methods: {
updateTokens() {
this.filterManager.updateTokens();
+ },
+ updateDetailIssue(newIssue) {
+ const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint;
+ if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
+ newIssue.setFetchingState('subscriptions', true);
+ BoardService.getIssueInfo(sidebarInfoEndpoint)
+ .then(res => res.json())
+ .then((data) => {
+ newIssue.setFetchingState('subscriptions', false);
+ newIssue.updateData({
+ subscribed: data.subscribed,
+ });
+ })
+ .catch(() => {
+ newIssue.setFetchingState('subscriptions', false);
+ Flash(__('An error occurred while fetching sidebar data'));
+ });
+ }
+
+ Store.detail.issue = newIssue;
+ },
+ clearDetailIssue() {
+ Store.detail.issue = {};
+ },
+ toggleSubscription(id) {
+ const issue = Store.detail.issue;
+ if (issue.id === id && issue.toggleSubscriptionEndpoint) {
+ issue.setFetchingState('subscriptions', true);
+ BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
+ .then(() => {
+ issue.setFetchingState('subscriptions', false);
+ issue.updateData({
+ subscribed: !issue.subscribed,
+ });
+ })
+ .catch(() => {
+ issue.setFetchingState('subscriptions', false);
+ Flash(__('An error occurred when toggling the notification subscription'));
+ });
+ }
}
},
});
diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.vue
index 079fb6438b9..0b220a56e0b 100644
--- a/app/assets/javascripts/boards/components/board_card.js
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,25 +1,11 @@
+<script>
import './issue_card_inner';
+import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
export default {
name: 'BoardsIssueCard',
- template: `
- <li class="card"
- :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
- :index="index"
- :data-issue-id="issue.id"
- @mousedown="mouseDown"
- @mousemove="mouseMove"
- @mouseup="showIssue($event)">
- <issue-card-inner
- :list="list"
- :issue="issue"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- :update-filters="true" />
- </li>
- `,
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
@@ -56,12 +42,30 @@ export default {
this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
- Store.detail.issue = {};
+ eventHub.$emit('clearDetailIssue');
} else {
- Store.detail.issue = this.issue;
+ eventHub.$emit('newDetailIssue', this.issue);
Store.detail.list = this.list;
}
}
},
},
};
+</script>
+
+<template>
+ <li class="card"
+ :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
+ :index="index"
+ :data-issue-id="issue.id"
+ @mousedown="mouseDown"
+ @mousemove="mouseMove"
+ @mouseup="showIssue($event)">
+ <issue-card-inner
+ :list="list"
+ :issue="issue"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"
+ :update-filters="true" />
+ </li>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js
index 6159680f1e6..29aeb8e84aa 100644
--- a/app/assets/javascripts/boards/components/board_list.js
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -1,6 +1,6 @@
/* global Sortable */
import boardNewIssue from './board_new_issue';
-import boardCard from './board_card';
+import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 9ae5e270a4b..faa76da964f 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -5,12 +5,13 @@
import Vue from 'vue';
import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub';
-import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
-import Assignees from '../../sidebar/components/assignees/assignees';
+import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
+import assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
+import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
const Store = gl.issueBoards.BoardsStore;
@@ -117,11 +118,11 @@ gl.issueBoards.BoardSidebar = Vue.extend({
new DueDateSelectors();
new LabelsSelect();
new Sidebar();
- gl.Subscription.bindAll('.subscription');
},
components: {
+ assigneeTitle,
+ assignees,
removeBtn: gl.issueBoards.RemoveIssueBtn,
- 'assignee-title': AssigneeTitle,
- assignees: Assignees,
+ subscriptions,
},
});
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 407db176446..10f85c1d676 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -17,6 +17,11 @@ class ListIssue {
this.assignees = [];
this.selected = false;
this.position = obj.relative_position || Infinity;
+ this.isFetching = {
+ subscriptions: true,
+ };
+ this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
+ this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
@@ -73,6 +78,14 @@ class ListIssue {
return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
}
+ updateData(newData) {
+ Object.assign(this, newData);
+ }
+
+ setFetchingState(key, value) {
+ this.isFetching[key] = value;
+ }
+
update (url) {
const data = {
issue: {
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index df2809e1805..52a4e293bb6 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,6 +1,7 @@
/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
/* global ListIssue */
/* global ListLabel */
+import Vue from 'vue';
import queryData from '../utils/query_data';
const PER_PAGE = 20;
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 97e80afa3f8..fa7ddd25e1f 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -2,7 +2,7 @@
import Vue from 'vue';
-class BoardService {
+export default class BoardService {
constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: {
@@ -88,6 +88,14 @@ class BoardService {
return this.issues.bulkUpdate(data);
}
+
+ static getIssueInfo(endpoint) {
+ return Vue.http.get(endpoint);
+ }
+
+ static toggleIssueSubscription(endpoint) {
+ return Vue.http.post(endpoint);
+ }
}
window.BoardService = BoardService;
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
index 1191e0b895e..ada693afc46 100644
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -14,7 +14,6 @@ export default () => {
});
new LabelsSelect();
new IssuableContext(sidebarOptions.currentUser);
- gl.Subscription.bindAll('.subscription');
new DueDateSelectors();
window.sidebar = new Sidebar();
};
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 0035dd23011..902d9436b83 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -85,7 +85,6 @@ import './right_sidebar';
import './search';
import './search_autocomplete';
import './smart_interval';
-import './subscription';
import './subscription_select';
import initBreadcrumbs from './breadcrumb';
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
index 4ad3d469f25..25acc099699 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -3,6 +3,7 @@ import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
import Flash from '../../../flash';
+import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue';
export default {
@@ -21,7 +22,7 @@ export default {
onToggleSubscription() {
this.mediator.toggleSubscription()
.catch(() => {
- Flash('Error occurred when toggling the notification subscription');
+ Flash(__('Error occurred when toggling the notification subscription'));
});
},
},
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index a3a8213d63a..940e1764f3d 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -14,6 +14,10 @@ export default {
type: Boolean,
required: false,
},
+ id: {
+ type: Number,
+ required: false,
+ },
},
components: {
loadingButton,
@@ -32,7 +36,7 @@ export default {
},
methods: {
toggleSubscription() {
- eventHub.$emit('toggleSubscription');
+ eventHub.$emit('toggleSubscription', this.id);
},
},
};
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
deleted file mode 100644
index bb4d68fcd49..00000000000
--- a/app/assets/javascripts/subscription.js
+++ /dev/null
@@ -1,45 +0,0 @@
-class Subscription {
- constructor(containerElm) {
- this.containerElm = containerElm;
-
- const subscribeButton = containerElm.querySelector('.js-subscribe-button');
- if (subscribeButton) {
- // remove class so we don't bind twice
- subscribeButton.classList.remove('js-subscribe-button');
- subscribeButton.addEventListener('click', this.toggleSubscription.bind(this));
- }
- }
-
- toggleSubscription(event) {
- const button = event.currentTarget;
- const buttonSpan = button.querySelector('span');
- if (!buttonSpan || button.classList.contains('disabled')) {
- return;
- }
- button.classList.add('disabled');
-
- const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe';
- const toggleActionUrl = this.containerElm.dataset.url;
-
- $.post(toggleActionUrl, () => {
- button.classList.remove('disabled');
-
- // hack to allow this to work with the issue boards Vue object
- if (document.querySelector('html').classList.contains('issue-boards-page')) {
- gl.issueBoards.boardStoreIssueSet(
- 'subscribed',
- !gl.issueBoards.BoardsStore.detail.issue.subscribed,
- );
- } else {
- buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe';
- }
- });
- }
-
- static bindAll(selector) {
- [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm));
- }
-}
-
-window.gl = window.gl || {};
-window.gl.Subscription = Subscription;
diff --git a/app/views/shared/boards/components/sidebar/_notifications.html.haml b/app/views/shared/boards/components/sidebar/_notifications.html.haml
index 9b989c23cab..333dd1a00b4 100644
--- a/app/views/shared/boards/components/sidebar/_notifications.html.haml
+++ b/app/views/shared/boards/components/sidebar/_notifications.html.haml
@@ -1,7 +1,5 @@
- if current_user
- .block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" }
- %span.issuable-header-text.hide-collapsed.pull-left
- Notifications
- %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
- %span
- {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}}
+ .block.subscriptions
+ %subscriptions{ ":loading" => "issue.isFetching && issue.isFetching.subscriptions",
+ ":subscribed" => "issue.subscribed",
+ ":id" => "issue.id" }
diff --git a/changelogs/unreleased/39167-async-boards-sidebar.yml b/changelogs/unreleased/39167-async-boards-sidebar.yml
new file mode 100644
index 00000000000..dc77f1ad451
--- /dev/null
+++ b/changelogs/unreleased/39167-async-boards-sidebar.yml
@@ -0,0 +1,5 @@
+---
+title: Update Issue Boards to fetch the notification subscription status asynchronously
+merge_request:
+author:
+type: performance
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 9137ab82ff4..205900615c4 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -331,11 +331,29 @@ describe 'Issue Boards', :js do
context 'subscription' do
it 'changes issue subscription' do
click_card(card)
+ wait_for_requests
- page.within('.subscription') do
+ page.within('.subscriptions') do
click_button 'Subscribe'
wait_for_requests
- expect(page).to have_content("Unsubscribe")
+
+ expect(page).to have_content('Unsubscribe')
+ end
+ end
+
+ it 'has "Unsubscribe" button when already subscribed' do
+ create(:subscription, user: user, project: project, subscribable: issue2, subscribed: true)
+ visit project_board_path(project, board)
+ wait_for_requests
+
+ click_card(card)
+ wait_for_requests
+
+ page.within('.subscriptions') do
+ click_button 'Unsubscribe'
+ wait_for_requests
+
+ expect(page).to have_content('Subscribe')
end
end
end
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index 83b13b06dc1..8f607899b20 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -9,10 +9,11 @@
import Vue from 'vue';
import '~/boards/models/assignee';
+import eventHub from '~/boards/eventhub';
import '~/boards/models/list';
import '~/boards/models/label';
import '~/boards/stores/boards_store';
-import boardCard from '~/boards/components/board_card';
+import boardCard from '~/boards/components/board_card.vue';
import './mock_data';
describe('Board card', () => {
@@ -157,33 +158,35 @@ describe('Board card', () => {
});
it('sets detail issue to card issue on mouse up', () => {
+ spyOn(eventHub, '$emit');
+
triggerEvent('mousedown');
triggerEvent('mouseup');
- expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
+ expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue);
expect(gl.issueBoards.BoardsStore.detail.list).toEqual(vm.list);
});
it('adds active class if detail issue is set', (done) => {
- triggerEvent('mousedown');
- triggerEvent('mouseup');
-
- setTimeout(() => {
- expect(vm.$el.classList.contains('is-active')).toBe(true);
- done();
- }, 0);
+ vm.detailIssue.issue = vm.issue;
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.classList.contains('is-active')).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
});
it('resets detail issue to empty if already set', () => {
- triggerEvent('mousedown');
- triggerEvent('mouseup');
+ spyOn(eventHub, '$emit');
- expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
+ gl.issueBoards.BoardsStore.detail.issue = vm.issue;
triggerEvent('mousedown');
triggerEvent('mouseup');
- expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+ expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue');
});
});
});
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index 022d286d5df..ccde657789a 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -133,6 +133,19 @@ describe('Issue model', () => {
expect(relativePositionIssue.position).toBe(1);
});
+ it('updates data', () => {
+ issue.updateData({ subscribed: true });
+ expect(issue.subscribed).toBe(true);
+ });
+
+ it('sets fetching state', () => {
+ expect(issue.isFetching.subscriptions).toBe(true);
+
+ issue.setFetchingState('subscriptions', false);
+
+ expect(issue.isFetching.subscriptions).toBe(false);
+ });
+
describe('update', () => {
it('passes assignee ids when there are assignees', (done) => {
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
diff --git a/spec/javascripts/vue_shared/components/loading_button_spec.js b/spec/javascripts/vue_shared/components/loading_button_spec.js
index c1eabdede00..49bf8ee6f7c 100644
--- a/spec/javascripts/vue_shared/components/loading_button_spec.js
+++ b/spec/javascripts/vue_shared/components/loading_button_spec.js
@@ -98,7 +98,6 @@ describe('LoadingButton', function () {
it('does not call given callback when disabled because of loading', () => {
vm = mountComponent(LoadingButton, {
loading: true,
- indeterminate: true,
});
spyOn(vm, '$emit');