boards_bundle.js 11.3 KB
Newer Older
1
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
2

3
import _ from 'underscore';
4
import Vue from 'vue';
Phil Hughes's avatar
Phil Hughes committed
5
import Flash from '../flash';
6
import { __ } from '../locale';
7
import FilteredSearchBoards from './filtered_search_boards';
8
import eventHub from './eventhub';
9
import sidebarEventHub from '../sidebar/event_hub';
10 11 12 13
import './models/issue';
import './models/label';
import './models/list';
import './models/milestone';
14
import './models/project';
15 16 17
import './models/assignee';
import './stores/boards_store';
import './stores/modal_store';
18
import BoardService from './services/board_service';
19 20 21 22 23 24 25 26 27 28
import './mixins/modal_mixins';
import './mixins/sortable_default_options';
import './filters/due_date_filters';
import './components/board';
import './components/board_sidebar';
import './components/new_list_dropdown';
import './components/modal/index';
import '../vue_shared/vue_resource_interceptor';

import './components/boards_selector';
29 30
import collapseIcon from './icons/fullscreen_collapse.svg';
import expandIcon from './icons/fullscreen_expand.svg';
Clement Ho's avatar
Clement Ho committed
31
import tooltip from '../vue_shared/directives/tooltip';
32

Phil Hughes's avatar
Phil Hughes committed
33
$(() => {
34 35
  const $boardApp = document.getElementById('board-app');
  const Store = gl.issueBoards.BoardsStore;
Phil Hughes's avatar
Phil Hughes committed
36
  const ModalStore = gl.issueBoards.ModalStore;
37
  const issueBoardsContent = document.querySelector('.content-wrapper > .js-focus-mode-board');
Phil Hughes's avatar
Phil Hughes committed
38

39
  window.gl = window.gl || {};
Phil Hughes's avatar
Phil Hughes committed
40

41 42 43 44
  if (gl.IssueBoardsApp) {
    gl.IssueBoardsApp.$destroy(true);
  }

45 46
  Store.create();

47 48 49 50
  // hack to allow sidebar scripts like milestone_select manipulate the BoardsStore
  gl.issueBoards.boardStoreIssueSet = (...args) => Vue.set(Store.detail.issue, ...args);
  gl.issueBoards.boardStoreIssueDelete = (...args) => Vue.delete(Store.detail.issue, ...args);

51
  gl.IssueBoardsApp = new Vue({
52
    el: $boardApp,
53
    components: {
54
      'board': gl.issueBoards.Board,
Phil Hughes's avatar
Phil Hughes committed
55 56
      'board-sidebar': gl.issueBoards.BoardSidebar,
      'board-add-issues-modal': gl.issueBoards.IssuesModal,
57
    },
Phil Hughes's avatar
Phil Hughes committed
58
    data: {
59
      state: Store.state,
Phil Hughes's avatar
Phil Hughes committed
60
      loading: true,
61 62
      boardsEndpoint: $boardApp.dataset.boardsEndpoint,
      listsEndpoint: $boardApp.dataset.listsEndpoint,
63
      boardId: $boardApp.dataset.boardId,
64
      disabled: $boardApp.dataset.disabled === 'true',
65
      issueLinkBase: $boardApp.dataset.issueLinkBase,
66
      rootPath: $boardApp.dataset.rootPath,
67
      bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
68
      detailIssue: Store.detail,
69
      defaultAvatar: $boardApp.dataset.defaultAvatar,
Phil Hughes's avatar
Phil Hughes committed
70
    },
71 72 73
    computed: {
      detailIssueVisible () {
        return Object.keys(this.detailIssue.issue).length;
74
      },
75
    },
Phil Hughes's avatar
Phil Hughes committed
76
    created () {
77 78 79 80 81 82
      gl.boardService = new BoardService({
        boardsEndpoint: this.boardsEndpoint,
        listsEndpoint: this.listsEndpoint,
        bulkUpdatePath: this.bulkUpdatePath,
        boardId: this.boardId,
      });
83
      Store.rootPath = this.boardsEndpoint;
84

85
      eventHub.$on('updateTokens', this.updateTokens);
86 87 88
      eventHub.$on('newDetailIssue', this.updateDetailIssue);
      eventHub.$on('clearDetailIssue', this.clearDetailIssue);
      sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
89
      sidebarEventHub.$on('updateWeight', this.updateWeight);
90 91
    },
    beforeDestroy() {
92
      eventHub.$off('updateTokens', this.updateTokens);
93 94 95
      eventHub.$off('newDetailIssue', this.updateDetailIssue);
      eventHub.$off('clearDetailIssue', this.clearDetailIssue);
      sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
96
      sidebarEventHub.$off('updateWeight', this.updateWeight);
97
    },
Fatih Acet's avatar
Fatih Acet committed
98
    mounted () {
99
      this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit);
100 101
      this.filterManager.setup();

102
      Store.disabled = this.disabled;
103
      gl.boardService.all()
Eric Eastwood's avatar
Eric Eastwood committed
104 105 106
        .then(res => res.data)
        .then((data) => {
          data.forEach((board) => {
107
            const list = Store.addList(board, this.defaultAvatar);
108

109
            if (list.type === 'closed') {
Phil Hughes's avatar
Phil Hughes committed
110
              list.position = Infinity;
111
              list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' };
112 113
            } else if (list.type === 'backlog') {
              list.position = -1;
114
            }
Phil Hughes's avatar
Phil Hughes committed
115
          });
116

Phil Hughes's avatar
Phil Hughes committed
117 118
          this.state.lists = _.sortBy(this.state.lists, 'position');

119
          Store.addBlankState();
120
          Store.addPromotionState();
121
          this.loading = false;
122
        })
Eric Eastwood's avatar
Eric Eastwood committed
123 124 125
        .catch(() => {
          Flash('An error occurred while fetching the board lists. Please try again.');
        });
126 127 128 129
    },
    methods: {
      updateTokens() {
        this.filterManager.updateTokens();
130 131 132 133 134
      },
      updateDetailIssue(newIssue) {
        const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint;
        if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
          newIssue.setFetchingState('subscriptions', true);
135
          newIssue.setFetchingState('weight', true);
136
          BoardService.getIssueInfo(sidebarInfoEndpoint)
Eric Eastwood's avatar
Eric Eastwood committed
137
            .then(res => res.data)
138 139
            .then((data) => {
              newIssue.setFetchingState('subscriptions', false);
140
              newIssue.setFetchingState('weight', false);
141 142
              newIssue.updateData({
                subscribed: data.subscribed,
143
                weight: data.weight,
144 145 146 147
              });
            })
            .catch(() => {
              newIssue.setFetchingState('subscriptions', false);
148
              newIssue.setFetchingState('weight', false);
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
              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'));
            });
        }
174 175 176 177 178 179
      },
      updateWeight(newWeight, id) {
        const issue = Store.detail.issue;
        if (issue.id === id && issue.sidebarInfoEndpoint) {
          issue.setLoadingState('weight', true);
          BoardService.updateWeight(issue.sidebarInfoEndpoint, newWeight)
Eric Eastwood's avatar
Eric Eastwood committed
180
            .then(res => res.data)
181 182 183 184 185 186 187 188 189 190 191
            .then((data) => {
              issue.setLoadingState('weight', false);
              issue.updateData({
                weight: data.weight,
              });
            })
            .catch(() => {
              issue.setLoadingState('weight', false);
              Flash(__('An error occurred when updating the issue weight'));
            });
        }
192 193
      }
    },
Phil Hughes's avatar
Phil Hughes committed
194
  });
195 196

  gl.IssueBoardsSearch = new Vue({
197
    el: document.getElementById('js-add-list'),
198
    data: {
199 200
      filters: Store.state.filters,
      milestoneTitle: $boardApp.dataset.boardMilestoneTitle,
201 202 203
    },
    mounted () {
      gl.issueBoards.newListDropdownInit();
204
    },
205
  });
Phil Hughes's avatar
Phil Hughes committed
206

207 208 209 210 211 212 213
  const configEl = document.querySelector('.js-board-config');

  if (configEl) {
    gl.boardConfigToggle = new Vue({
      el: configEl,
      data() {
        return {
Clement Ho's avatar
Clement Ho committed
214 215 216
          canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
          hasScope: this.$options.el.hasAttribute('data-has-scope'),
          state: Store.state,
217
        };
218
      },
Clement Ho's avatar
Clement Ho committed
219 220 221
      directives: {
        tooltip,
      },
222 223 224 225 226 227 228
      methods: {
        showPage: page => gl.issueBoards.BoardsStore.showPage(page),
      },
      computed: {
        buttonText() {
          return this.canAdminList ? 'Edit board' : 'View scope';
        },
Clement Ho's avatar
Clement Ho committed
229 230 231
        tooltipTitle() {
          return this.hasScope ? __('This board\'s scope is reduced') : '';
        }
232 233 234 235
      },
      template: `
        <div class="prepend-left-10">
          <button
Clement Ho's avatar
Clement Ho committed
236 237
            v-tooltip
            :title="tooltipTitle"
238
            class="btn btn-inverted"
Clement Ho's avatar
Clement Ho committed
239
            :class="{ 'dot-highlight': hasScope }"
240 241 242 243 244 245 246 247 248
            type="button"
            @click.prevent="showPage('edit')"
          >
            {{ buttonText }}
          </button>
        </div>
      `,
    });
  }
249

250
  gl.IssueBoardsModalAddBtn = new Vue({
251
    mixins: [gl.issueBoards.ModalMixins],
252
    el: document.getElementById('js-add-issues-btn'),
253 254 255 256 257
    data() {
      return {
        modal: ModalStore.store,
        store: Store.state,
        isFullscreen: false,
Clement Ho's avatar
Clement Ho committed
258 259
        focusModeAvailable: $boardApp.hasAttribute('data-focus-mode-available'),
        canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
260
      };
261
    },
262 263 264 265 266
    watch: {
      disabled() {
        this.updateTooltip();
      },
    },
267 268
    computed: {
      disabled() {
269 270 271
        if (!this.store) {
          return true;
        }
Phil Hughes's avatar
Phil Hughes committed
272
        return !this.store.lists.filter(list => !list.preset).length;
273
      },
274 275 276 277 278 279 280 281 282 283
      tooltipTitle() {
        if (this.disabled) {
          return 'Please add a list to your board first';
        }

        return '';
      },
    },
    methods: {
      updateTooltip() {
284
        const $tooltip = $(this.$refs.addIssuesButton);
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301

        this.$nextTick(() => {
          if (this.disabled) {
            $tooltip.tooltip();
          } else {
            $tooltip.tooltip('destroy');
          }
        });
      },
      openModal() {
        if (!this.disabled) {
          this.toggleModal(true);
        }
      },
    },
    mounted() {
      this.updateTooltip();
302 303
    },
    template: `
304 305 306 307 308 309 310 311 312
      <div class="board-extra-actions">
        <button
          class="btn btn-create prepend-left-10"
          type="button"
          data-placement="bottom"
          ref="addIssuesButton"
          :class="{ 'disabled': disabled }"
          :title="tooltipTitle"
          :aria-disabled="disabled"
313
          v-if="canAdminList"
314 315 316
          @click="openModal">
          Add issues
        </button>
317 318 319 320 321 322 323 324 325 326
      </div>
    `,
  });

  gl.IssueBoardsToggleFocusBtn = new Vue({
    el: document.getElementById('js-toggle-focus-btn'),
    data: {
      modal: ModalStore.store,
      store: Store.state,
      isFullscreen: false,
Clement Ho's avatar
Clement Ho committed
327
      focusModeAvailable: $boardApp.hasAttribute('data-focus-mode-available'),
328 329 330 331 332 333 334 335 336 337 338 339 340
    },
    methods: {
      toggleFocusMode() {
        if (!this.focusModeAvailable) { return; }

        $(this.$refs.toggleFocusModeButton).tooltip('hide');
        issueBoardsContent.classList.toggle('is-focused');

        this.isFullscreen = !this.isFullscreen;
      },
    },
    template: `
      <div class="board-extra-actions">
341 342
        <a
          href="#"
343
          class="btn btn-default has-tooltip prepend-left-10 js-focus-mode-btn"
344 345 346 347
          role="button"
          aria-label="Toggle focus mode"
          title="Toggle focus mode"
          ref="toggleFocusModeButton"
348
          v-if="focusModeAvailable"
349 350 351 352 353 354 355 356 357
          @click="toggleFocusMode">
          <span v-show="isFullscreen">
            ${collapseIcon}
          </span>
          <span v-show="!isFullscreen">
            ${expandIcon}
          </span>
        </a>
      </div>
358 359
    `,
  });
360 361 362 363 364 365 366

  gl.IssueboardsSwitcher = new Vue({
    el: '#js-multiple-boards-switcher',
    components: {
      'boards-selector': gl.issueBoards.BoardsSelector,
    }
  });
Phil Hughes's avatar
Phil Hughes committed
367
});