From f3433d8a6801d369752526c1d92a35048e263e20 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Sun, 3 Dec 2017 20:43:10 -0600 Subject: [PATCH] Backport changes from refactor sidebar weight block Vue and move to Issue Boards See https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3566 --- app/assets/javascripts/boards/models/issue.js | 5 + app/assets/javascripts/gl_dropdown.js | 3 +- app/assets/javascripts/right_sidebar.js | 6 +- .../javascripts/sidebar/mount_sidebar.js | 104 ++++++++++++++++++ .../javascripts/sidebar/sidebar_bundle.js | 102 +---------------- .../javascripts/sidebar/sidebar_mediator.js | 34 +++--- .../sidebar/stores/sidebar_store.js | 5 + spec/javascripts/boards/issue_spec.js | 6 + spec/javascripts/sidebar/mock_data.js | 84 ++++++++++++-- .../sidebar/sidebar_mediator_spec.js | 27 ++++- .../javascripts/sidebar/sidebar_store_spec.js | 6 + 11 files changed, 250 insertions(+), 132 deletions(-) create mode 100644 app/assets/javascripts/sidebar/mount_sidebar.js diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 10f85c1d676..81edd95bf2b 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -20,6 +20,7 @@ class ListIssue { this.isFetching = { subscriptions: true, }; + this.isLoading = {}; this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint; this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint; @@ -86,6 +87,10 @@ class ListIssue { this.isFetching[key] = value; } + setLoadingState(key, value) { + this.isLoading[key] = value; + } + update (url) { const data = { issue: { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 4e7a6e54f90..7ca783d3af6 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -514,10 +514,11 @@ GitLabDropdown = (function() { const dropdownToggle = this.dropdown.find('.dropdown-menu-toggle'); const hasFilterBulkUpdate = dropdownToggle.hasClass('js-filter-bulk-update'); + const shouldRefreshOnOpen = dropdownToggle.hasClass('js-gl-dropdown-refresh-on-open'); const hasMultiSelect = dropdownToggle.hasClass('js-multiselect'); // Makes indeterminate items effective - if (this.fullData && hasFilterBulkUpdate) { + if (this.fullData && (shouldRefreshOnOpen || hasFilterBulkUpdate)) { this.parseData(this.fullData); } diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index a41548bd694..fa7f6825d7e 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -15,7 +15,7 @@ import Cookies from 'js-cookie'; Sidebar.prototype.removeListeners = function () { this.sidebar.off('click', '.sidebar-collapsed-icon'); - $('.dropdown').off('hidden.gl.dropdown'); + this.sidebar.off('hidden.gl.dropdown'); $('.dropdown').off('loading.gl.dropdown'); $('.dropdown').off('loaded.gl.dropdown'); $(document).off('click', '.js-sidebar-toggle'); @@ -25,7 +25,7 @@ import Cookies from 'js-cookie'; const $document = $(document); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); - $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); + this.sidebar.on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); @@ -180,7 +180,7 @@ import Cookies from 'js-cookie'; var $block, sidebar; sidebar = e.data; e.preventDefault(); - $block = $(this).closest('.block'); + $block = $(e.target).closest('.block'); return sidebar.sidebarDropdownHidden($block); }; diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js new file mode 100644 index 00000000000..4032f156b15 --- /dev/null +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -0,0 +1,104 @@ +import Vue from 'vue'; +import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking'; +import SidebarAssignees from './components/assignees/sidebar_assignees'; +import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue'; +import SidebarMoveIssue from './lib/sidebar_move_issue'; +import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue'; +import sidebarParticipants from './components/participants/sidebar_participants.vue'; +import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue'; +import Translate from '../vue_shared/translate'; + +Vue.use(Translate); + +function mountConfidentialComponent(mediator) { + const el = document.getElementById('js-confidential-entry-point'); + + if (!el) return; + + const dataNode = document.getElementById('js-confidential-issue-data'); + const initialData = JSON.parse(dataNode.innerHTML); + + const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar); + + new ConfidentialComp({ + propsData: { + isConfidential: initialData.is_confidential, + isEditable: initialData.is_editable, + service: mediator.service, + }, + }).$mount(el); +} + +function mountLockComponent(mediator) { + const el = document.getElementById('js-lock-entry-point'); + + if (!el) return; + + const dataNode = document.getElementById('js-lock-issue-data'); + const initialData = JSON.parse(dataNode.innerHTML); + + const LockComp = Vue.extend(LockIssueSidebar); + + new LockComp({ + propsData: { + isLocked: initialData.is_locked, + isEditable: initialData.is_editable, + mediator, + issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request', + }, + }).$mount(el); +} + +function mountParticipantsComponent() { + const el = document.querySelector('.js-sidebar-participants-entry-point'); + + if (!el) return; + + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + sidebarParticipants, + }, + render: createElement => createElement('sidebar-participants', {}), + }); +} + +function mountSubscriptionsComponent() { + const el = document.querySelector('.js-sidebar-subscriptions-entry-point'); + + if (!el) return; + + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + sidebarSubscriptions, + }, + render: createElement => createElement('sidebar-subscriptions', {}), + }); +} + +function mount(mediator) { + const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees'); + // Only create the sidebarAssignees vue app if it is found in the DOM + // We currently do not use sidebarAssignees for the MR page + if (sidebarAssigneesEl) { + new Vue(SidebarAssignees).$mount(sidebarAssigneesEl); + } + + mountConfidentialComponent(mediator); + mountLockComponent(mediator); + mountParticipantsComponent(); + mountSubscriptionsComponent(); + + new SidebarMoveIssue( + mediator, + $('.js-move-issue'), + $('.js-move-issue-confirmation-button'), + ).init(); + + new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker'); +} + +export default mount; diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js index 2650bb725d4..f78287e504b 100644 --- a/app/assets/javascripts/sidebar/sidebar_bundle.js +++ b/app/assets/javascripts/sidebar/sidebar_bundle.js @@ -1,110 +1,12 @@ -import Vue from 'vue'; -import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking'; -import SidebarAssignees from './components/assignees/sidebar_assignees'; -import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue'; -import SidebarMoveIssue from './lib/sidebar_move_issue'; -import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue'; -import sidebarParticipants from './components/participants/sidebar_participants.vue'; -import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue'; -import Translate from '../vue_shared/translate'; - import Mediator from './sidebar_mediator'; - -Vue.use(Translate); - -function mountConfidentialComponent(mediator) { - const el = document.getElementById('js-confidential-entry-point'); - - if (!el) return; - - const dataNode = document.getElementById('js-confidential-issue-data'); - const initialData = JSON.parse(dataNode.innerHTML); - - const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar); - - new ConfidentialComp({ - propsData: { - isConfidential: initialData.is_confidential, - isEditable: initialData.is_editable, - service: mediator.service, - }, - }).$mount(el); -} - -function mountLockComponent(mediator) { - const el = document.getElementById('js-lock-entry-point'); - - if (!el) return; - - const dataNode = document.getElementById('js-lock-issue-data'); - const initialData = JSON.parse(dataNode.innerHTML); - - const LockComp = Vue.extend(LockIssueSidebar); - - new LockComp({ - propsData: { - isLocked: initialData.is_locked, - isEditable: initialData.is_editable, - mediator, - issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request', - }, - }).$mount(el); -} - -function mountParticipantsComponent() { - const el = document.querySelector('.js-sidebar-participants-entry-point'); - - if (!el) return; - - // eslint-disable-next-line no-new - new Vue({ - el, - components: { - sidebarParticipants, - }, - render: createElement => createElement('sidebar-participants', {}), - }); -} - -function mountSubscriptionsComponent() { - const el = document.querySelector('.js-sidebar-subscriptions-entry-point'); - - if (!el) return; - - // eslint-disable-next-line no-new - new Vue({ - el, - components: { - sidebarSubscriptions, - }, - render: createElement => createElement('sidebar-subscriptions', {}), - }); -} +import mountSidebar from './mount_sidebar'; function domContentLoaded() { const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); const mediator = new Mediator(sidebarOptions); mediator.fetch(); - const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees'); - // Only create the sidebarAssignees vue app if it is found in the DOM - // We currently do not use sidebarAssignees for the MR page - if (sidebarAssigneesEl) { - new Vue(SidebarAssignees).$mount(sidebarAssigneesEl); - } - - mountConfidentialComponent(mediator); - mountLockComponent(mediator); - mountParticipantsComponent(); - mountSubscriptionsComponent(); - - new SidebarMoveIssue( - mediator, - $('.js-move-issue'), - $('.js-move-issue-confirmation-button'), - ).init(); - - new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker'); + mountSidebar(mediator); } document.addEventListener('DOMContentLoaded', domContentLoaded); diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index 2bda5a47791..d4c07a188b3 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -5,19 +5,23 @@ import Store from './stores/sidebar_store'; export default class SidebarMediator { constructor(options) { if (!SidebarMediator.singleton) { - this.store = new Store(options); - this.service = new Service({ - endpoint: options.endpoint, - toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint, - moveIssueEndpoint: options.moveIssueEndpoint, - projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint, - }); - SidebarMediator.singleton = this; + this.initSingleton(options); } return SidebarMediator.singleton; } + initSingleton(options) { + this.store = new Store(options); + this.service = new Service({ + endpoint: options.endpoint, + toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint, + moveIssueEndpoint: options.moveIssueEndpoint, + projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint, + }); + SidebarMediator.singleton = this; + } + assignYourself() { this.store.addAssignee(this.store.currentUser); } @@ -35,17 +39,21 @@ export default class SidebarMediator { } fetch() { - this.service.get() + return this.service.get() .then(response => response.json()) .then((data) => { - this.store.setAssigneeData(data); - this.store.setTimeTrackingData(data); - this.store.setParticipantsData(data); - this.store.setSubscriptionsData(data); + this.processFetchedData(data); }) .catch(() => new Flash('Error occurred when fetching sidebar data')); } + processFetchedData(data) { + this.store.setAssigneeData(data); + this.store.setTimeTrackingData(data); + this.store.setParticipantsData(data); + this.store.setSubscriptionsData(data); + } + toggleSubscription() { this.store.setFetchingState('subscriptions', true); return this.service.toggleSubscription() diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js index 3150221b685..73eb25e2333 100644 --- a/app/assets/javascripts/sidebar/stores/sidebar_store.js +++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js @@ -15,6 +15,7 @@ export default class SidebarStore { participants: true, subscriptions: true, }; + this.isLoading = {}; this.autocompleteProjects = []; this.moveToProjectId = 0; this.isLockDialogOpen = false; @@ -55,6 +56,10 @@ export default class SidebarStore { this.isFetching[key] = value; } + setLoadingState(key, value) { + this.isLoading[key] = value; + } + addAssignee(assignee) { if (!this.findAssignee(assignee)) { this.assignees.push(assignee); diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index ccde657789a..10b88878c2a 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -146,6 +146,12 @@ describe('Issue model', () => { expect(issue.isFetching.subscriptions).toBe(false); }); + it('sets loading state', () => { + issue.setLoadingState('foo', true); + + expect(issue.isLoading.foo).toBe(true); + }); + describe('update', () => { it('passes assignee ids when there are assignees', (done) => { spyOn(Vue.http, 'patch').and.callFake((url, data) => { diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js index 0682b463043..3b094d20838 100644 --- a/spec/javascripts/sidebar/mock_data.js +++ b/spec/javascripts/sidebar/mock_data.js @@ -1,6 +1,6 @@ /* eslint-disable quote-props*/ -const sidebarMockData = { +const RESPONSE_MAP = { 'GET': { '/gitlab-org/gitlab-shell/issues/5.json': { id: 45, @@ -66,6 +66,65 @@ const sidebarMockData = { }, labels: [], }, + '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar': { + assignees: [ + { + name: 'User 0', + username: 'user0', + id: 22, + state: 'active', + avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/user0', + }, + { + name: 'Marguerite Bartell', + username: 'tajuana', + id: 18, + state: 'active', + avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/tajuana', + }, + { + name: 'Laureen Ritchie', + username: 'michaele.will', + id: 16, + state: 'active', + avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/michaele.will', + }, + ], + human_time_estimate: null, + human_total_time_spent: null, + participants: [ + { + name: 'User 0', + username: 'user0', + id: 22, + state: 'active', + avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/user0', + }, + { + name: 'Marguerite Bartell', + username: 'tajuana', + id: 18, + state: 'active', + avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/tajuana', + }, + { + name: 'Laureen Ritchie', + username: 'michaele.will', + id: 16, + state: 'active', + avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/michaele.will', + }, + ], + subscribed: true, + time_estimate: 0, + total_time_spent: 0, + }, '/autocomplete/projects?project_id=15': [ { 'id': 0, @@ -113,9 +172,10 @@ const sidebarMockData = { }, }; -export default { +const mockData = { + responseMap: RESPONSE_MAP, mediator: { - endpoint: '/gitlab-org/gitlab-shell/issues/5.json', + endpoint: '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar', toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription', moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move', projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15', @@ -141,12 +201,14 @@ export default { name: 'Administrator', username: 'root', }, - - sidebarMockInterceptor(request, next) { - const body = sidebarMockData[request.method.toUpperCase()][request.url]; - - next(request.respondWith(JSON.stringify(body), { - status: 200, - })); - }, }; + +mockData.sidebarMockInterceptor = function (request, next) { + const body = this.responseMap[request.method.toUpperCase()][request.url]; + + next(request.respondWith(JSON.stringify(body), { + status: 200, + })); +}.bind(mockData); + +export default mockData; diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js index 7deb1fd2118..14c34d5a78c 100644 --- a/spec/javascripts/sidebar/sidebar_mediator_spec.js +++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js @@ -33,10 +33,29 @@ describe('Sidebar mediator', () => { .catch(done.fail); }); - it('fetches the data', () => { - spyOn(this.mediator.service, 'get').and.callThrough(); - this.mediator.fetch(); - expect(this.mediator.service.get).toHaveBeenCalled(); + it('fetches the data', (done) => { + const mockData = Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar']; + spyOn(this.mediator, 'processFetchedData').and.callThrough(); + + this.mediator.fetch() + .then(() => { + expect(this.mediator.processFetchedData).toHaveBeenCalledWith(mockData); + }) + .then(done) + .catch(done.fail); + }); + + it('processes fetched data', () => { + const mockData = Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar']; + this.mediator.processFetchedData(mockData); + + expect(this.mediator.store.assignees).toEqual(mockData.assignees); + expect(this.mediator.store.humanTimeEstimate).toEqual(mockData.human_time_estimate); + expect(this.mediator.store.humanTotalTimeSpent).toEqual(mockData.human_total_time_spent); + expect(this.mediator.store.participants).toEqual(mockData.participants); + expect(this.mediator.store.subscribed).toEqual(mockData.subscribed); + expect(this.mediator.store.timeEstimate).toEqual(mockData.time_estimate); + expect(this.mediator.store.totalTimeSpent).toEqual(mockData.total_time_spent); }); it('sets moveToProjectId', () => { diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js index 51dee64fb93..ea4eae1e23f 100644 --- a/spec/javascripts/sidebar/sidebar_store_spec.js +++ b/spec/javascripts/sidebar/sidebar_store_spec.js @@ -120,6 +120,12 @@ describe('Sidebar store', () => { expect(this.store.isFetching.participants).toEqual(false); }); + it('sets loading state', () => { + this.store.setLoadingState('assignees', true); + + expect(this.store.isLoading.assignees).toEqual(true); + }); + it('set time tracking data', () => { this.store.setTimeTrackingData(Mock.time); expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate);