actions.js 11.5 KB
Newer Older
1
import Vue from 'vue';
2
import $ from 'jquery';
Felipe Artur's avatar
Felipe Artur committed
3
import axios from '~/lib/utils/axios_utils';
4
import Visibility from 'visibilityjs';
5
import TaskList from '../../task_list';
Phil Hughes's avatar
Phil Hughes committed
6
import Flash from '../../flash';
7
import Poll from '../../lib/utils/poll';
8
import * as types from './mutation_types';
Filipa Lacerda's avatar
Filipa Lacerda committed
9 10
import * as utils from './utils';
import * as constants from '../constants';
Simon Knox's avatar
Simon Knox committed
11
import service from '../services/notes_service';
12 13
import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
14
import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
15
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
16
import { __ } from '~/locale';
17

18 19
let eTagPoll;

Felipe Artur's avatar
Felipe Artur committed
20 21
export const expandDiscussion = ({ commit }, data) => commit(types.EXPAND_DISCUSSION, data);

22 23
export const collapseDiscussion = ({ commit }, data) => commit(types.COLLAPSE_DISCUSSION, data);

Lukas Eipert's avatar
Lukas Eipert committed
24
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
Felipe Artur's avatar
Felipe Artur committed
25

Lukas Eipert's avatar
Lukas Eipert committed
26
export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
Felipe Artur's avatar
Felipe Artur committed
27

Lukas Eipert's avatar
Lukas Eipert committed
28
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
Felipe Artur's avatar
Felipe Artur committed
29

Lukas Eipert's avatar
Lukas Eipert committed
30
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
Felipe Artur's avatar
Felipe Artur committed
31 32 33 34

export const setInitialNotes = ({ commit }, discussions) =>
  commit(types.SET_INITIAL_DISCUSSIONS, discussions);

Lukas Eipert's avatar
Lukas Eipert committed
35
export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
Felipe Artur's avatar
Felipe Artur committed
36

37 38 39
export const setNotesFetchedState = ({ commit }, state) =>
  commit(types.SET_NOTES_FETCHED_STATE, state);

Lukas Eipert's avatar
Lukas Eipert committed
40
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
Fatih Acet's avatar
Fatih Acet committed
41

42
export const fetchDiscussions = ({ commit }, { path, filter }) =>
Fatih Acet's avatar
Fatih Acet committed
43
  service
44
    .fetchDiscussions(path, filter)
Fatih Acet's avatar
Fatih Acet committed
45
    .then(res => res.json())
Felipe Artur's avatar
Felipe Artur committed
46 47
    .then(discussions => {
      commit(types.SET_INITIAL_DISCUSSIONS, discussions);
Fatih Acet's avatar
Fatih Acet committed
48
    });
49

50
export const updateDiscussion = ({ commit, state }, discussion) => {
51 52
  commit(types.UPDATE_DISCUSSION, discussion);

53
  return utils.findNoteObjectById(state.discussions, discussion.id);
54
};
55

56
export const deleteNote = ({ commit, dispatch }, note) =>
Fatih Acet's avatar
Fatih Acet committed
57
  service.deleteNote(note.path).then(() => {
58
    commit(types.DELETE_NOTE, note);
59 60

    dispatch('updateMergeRequestWidget');
61 62
  });

63
export const updateNote = ({ commit, dispatch }, { endpoint, note }) =>
Fatih Acet's avatar
Fatih Acet committed
64 65 66 67 68
  service
    .updateNote(endpoint, note)
    .then(res => res.json())
    .then(res => {
      commit(types.UPDATE_NOTE, res);
69
      dispatch('startTaskList');
Fatih Acet's avatar
Fatih Acet committed
70
    });
71

Fatih Acet's avatar
Fatih Acet committed
72 73 74 75 76 77
export const replyToDiscussion = ({ commit }, { endpoint, data }) =>
  service
    .replyToDiscussion(endpoint, data)
    .then(res => res.json())
    .then(res => {
      commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
78

Fatih Acet's avatar
Fatih Acet committed
79 80
      return res;
    });
81

82
export const createNewNote = ({ commit, dispatch }, { endpoint, data }) =>
Fatih Acet's avatar
Fatih Acet committed
83 84 85 86 87 88
  service
    .createNewNote(endpoint, data)
    .then(res => res.json())
    .then(res => {
      if (!res.errors) {
        commit(types.ADD_NEW_NOTE, res);
89 90

        dispatch('updateMergeRequestWidget');
91
        dispatch('startTaskList');
Fatih Acet's avatar
Fatih Acet committed
92 93 94
      }
      return res;
    });
95

Lukas Eipert's avatar
Lukas Eipert committed
96
export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES);
97

98
export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, discussion }) =>
Fatih Acet's avatar
Fatih Acet committed
99 100 101 102
  service
    .toggleResolveNote(endpoint, isResolved)
    .then(res => res.json())
    .then(res => {
Lukas Eipert's avatar
Lukas Eipert committed
103
      const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
104

Fatih Acet's avatar
Fatih Acet committed
105
      commit(mutationType, res);
106 107

      dispatch('updateMergeRequestWidget');
Fatih Acet's avatar
Fatih Acet committed
108
    });
109

110 111 112
export const closeIssue = ({ commit, dispatch, state }) => {
  dispatch('toggleStateButtonLoading', true);
  return service
Fatih Acet's avatar
Fatih Acet committed
113 114 115 116 117 118 119
    .toggleIssueState(state.notesData.closePath)
    .then(res => res.json())
    .then(data => {
      commit(types.CLOSE_ISSUE);
      dispatch('emitStateChangedEvent', data);
      dispatch('toggleStateButtonLoading', false);
    });
120
};
121

122 123 124
export const reopenIssue = ({ commit, dispatch, state }) => {
  dispatch('toggleStateButtonLoading', true);
  return service
Fatih Acet's avatar
Fatih Acet committed
125 126 127 128 129 130 131
    .toggleIssueState(state.notesData.reopenPath)
    .then(res => res.json())
    .then(data => {
      commit(types.REOPEN_ISSUE);
      dispatch('emitStateChangedEvent', data);
      dispatch('toggleStateButtonLoading', false);
    });
132 133 134 135
};

export const toggleStateButtonLoading = ({ commit }, value) =>
  commit(types.TOGGLE_STATE_BUTTON_LOADING, value);
136

Lukas Eipert's avatar
Lukas Eipert committed
137
export const emitStateChangedEvent = ({ getters }, data) => {
Fatih Acet's avatar
Fatih Acet committed
138 139 140 141 142 143
  const event = new CustomEvent('issuable_vue_app:change', {
    detail: {
      data,
      isClosed: getters.openState === constants.CLOSED,
    },
  });
144 145 146 147 148 149 150 151 152 153 154 155

  document.dispatchEvent(event);
};

export const toggleIssueLocalState = ({ commit }, newState) => {
  if (newState === constants.CLOSED) {
    commit(types.CLOSE_ISSUE);
  } else if (newState === constants.REOPENED) {
    commit(types.REOPEN_ISSUE);
  }
};

156
export const saveNote = ({ commit, dispatch }, noteData) => {
Felipe Artur's avatar
Felipe Artur committed
157
  // For MR discussuions we need to post as `note[note]` and issue we use `note.note`.
158 159
  // For batch comments, we use draft_note
  const note = noteData.data.draft_note || noteData.data['note[note]'] || noteData.data.note.note;
160 161 162
  let placeholderText = note;
  const hasQuickActions = utils.hasQuickActions(placeholderText);
  const replyId = noteData.data.in_reply_to_discussion_id;
163 164 165 166 167 168 169 170 171 172 173 174 175
  let methodToDispatch;
  const postData = Object.assign({}, noteData);
  if (postData.isDraft === true) {
    methodToDispatch = replyId
      ? 'batchComments/addDraftToDiscussion'
      : 'batchComments/createNewDraft';
    if (!postData.draft_note && noteData.note) {
      postData.draft_note = postData.note;
      delete postData.note;
    }
  } else {
    methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote';
  }
176

177
  $('.notes-form .flash-container').hide(); // hide previous flash notification
178
  commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders
179

180 181 182 183
  if (replyId) {
    if (hasQuickActions) {
      placeholderText = utils.stripQuickActions(placeholderText);
    }
184

185 186 187 188 189 190
    if (placeholderText.length) {
      commit(types.SHOW_PLACEHOLDER_NOTE, {
        noteBody: placeholderText,
        replyId,
      });
    }
191

192 193 194 195 196 197 198
    if (hasQuickActions) {
      commit(types.SHOW_PLACEHOLDER_NOTE, {
        isSystemNote: true,
        noteBody: utils.getQuickActionText(note),
        replyId,
      });
    }
199 200
  }

201
  return dispatch(methodToDispatch, postData, { root: true }).then(res => {
Fatih Acet's avatar
Fatih Acet committed
202 203
    const { errors } = res;
    const commandsChanges = res.commands_changes;
204

Fatih Acet's avatar
Fatih Acet committed
205 206
    if (hasQuickActions && errors && Object.keys(errors).length) {
      eTagPoll.makeRequest();
207

Fatih Acet's avatar
Fatih Acet committed
208 209 210
      $('.js-gfm-input').trigger('clear-commands-cache.atwho');
      Flash('Commands applied', 'notice', noteData.flashContainer);
    }
211

Fatih Acet's avatar
Fatih Acet committed
212 213 214 215 216 217
    if (commandsChanges) {
      if (commandsChanges.emoji_award) {
        const votesBlock = $('.js-awards-block').eq(0);

        loadAwardsHandler()
          .then(awardsHandler => {
Lukas Eipert's avatar
Lukas Eipert committed
218
            awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award);
Fatih Acet's avatar
Fatih Acet committed
219 220 221 222 223 224 225 226 227
            awardsHandler.scrollToAwards();
          })
          .catch(() => {
            Flash(
              'Something went wrong while adding your award. Please try again.',
              'alert',
              noteData.flashContainer,
            );
          });
228 229
      }

Lukas Eipert's avatar
Lukas Eipert committed
230
      if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) {
Fatih Acet's avatar
Fatih Acet committed
231
        sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
232
      }
Fatih Acet's avatar
Fatih Acet committed
233
    }
234

Fatih Acet's avatar
Fatih Acet committed
235 236 237
    if (errors && errors.commands_only) {
      Flash(errors.commands_only, 'notice', noteData.flashContainer);
    }
238 239 240
    if (replyId) {
      commit(types.REMOVE_PLACEHOLDER_NOTES);
    }
Fatih Acet's avatar
Fatih Acet committed
241 242 243

    return res;
  });
244 245
};

Felipe Artur's avatar
Felipe Artur committed
246
const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
Filipa Lacerda's avatar
Filipa Lacerda committed
247
  if (resp.notes && resp.notes.length) {
248 249
    const { notesById } = getters;

Fatih Acet's avatar
Fatih Acet committed
250
    resp.notes.forEach(note => {
251 252
      if (notesById[note.id]) {
        commit(types.UPDATE_NOTE, note);
Lukas Eipert's avatar
Lukas Eipert committed
253
      } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
Felipe Artur's avatar
Felipe Artur committed
254
        const discussion = utils.findNoteObjectById(state.discussions, note.discussion_id);
255 256 257

        if (discussion) {
          commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
Felipe Artur's avatar
Felipe Artur committed
258
        } else if (note.type === constants.DIFF_NOTE) {
259
          dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
260 261 262
        } else {
          commit(types.ADD_NEW_NOTE, note);
        }
263 264 265 266
      } else {
        commit(types.ADD_NEW_NOTE, note);
      }
    });
267

268
    dispatch('startTaskList');
269 270
  }

271
  commit(types.SET_LAST_FETCHED_AT, resp.last_fetched_at);
272 273 274 275

  return resp;
};

Felipe Artur's avatar
Felipe Artur committed
276
export const poll = ({ commit, state, getters, dispatch }) => {
277
  eTagPoll = new Poll({
278 279
    resource: service,
    method: 'poll',
280
    data: state,
Fatih Acet's avatar
Fatih Acet committed
281
    successCallback: resp =>
Felipe Artur's avatar
Felipe Artur committed
282
      resp.json().then(data => pollSuccessCallBack(data, commit, state, getters, dispatch)),
Lukas Eipert's avatar
Lukas Eipert committed
283
    errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
284 285 286 287 288
  });

  if (!Visibility.hidden()) {
    eTagPoll.makeRequest();
  } else {
289
    service.poll(state);
290 291 292 293 294 295 296
  }

  Visibility.change(() => {
    if (!Visibility.hidden()) {
      eTagPoll.restart();
    } else {
      eTagPoll.stop();
297 298
    }
  });
299
};
300

301 302 303 304 305 306 307 308
export const stopPolling = () => {
  eTagPoll.stop();
};

export const restartPolling = () => {
  eTagPoll.restart();
};

309
export const fetchData = ({ commit, state, getters }) => {
Fatih Acet's avatar
Fatih Acet committed
310 311 312 313
  const requestData = {
    endpoint: state.notesData.notesPath,
    lastFetchedAt: state.lastFetchedAt,
  };
314

Fatih Acet's avatar
Fatih Acet committed
315 316
  service
    .poll(requestData)
317 318 319 320 321
    .then(resp => resp.json)
    .then(data => pollSuccessCallBack(data, commit, state, getters))
    .catch(() => Flash('Something went wrong while fetching latest comments.'));
};

Lukas Eipert's avatar
Lukas Eipert committed
322
export const toggleAward = ({ commit, getters }, { awardName, noteId }) => {
323 324
  commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] });
};
325

Lukas Eipert's avatar
Lukas Eipert committed
326
export const toggleAwardRequest = ({ dispatch }, data) => {
327
  const { endpoint, awardName } = data;
328

329 330 331 332
  return service
    .toggleAward(endpoint, { name: awardName })
    .then(res => res.json())
    .then(() => {
333
      dispatch('toggleAward', data);
334 335 336 337
    });
};

export const scrollToNoteIfNeeded = (context, el) => {
338 339
  if (!isInViewport(el[0])) {
    scrollToElement(el);
340 341
  }
};
342

Felipe Artur's avatar
Felipe Artur committed
343 344 345 346 347 348 349 350
export const fetchDiscussionDiffLines = ({ commit }, discussion) =>
  axios.get(discussion.truncatedDiffLinesPath).then(({ data }) => {
    commit(types.SET_DISCUSSION_DIFF_LINES, {
      discussionId: discussion.id,
      diffLines: data.truncated_diff_lines,
    });
  });

351
export const updateMergeRequestWidget = () => {
352
  mrWidgetEventHub.$emit('mr.discussion.updated');
353 354
};

355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372
export const setLoadingState = ({ commit }, data) => {
  commit(types.SET_NOTES_LOADING_STATE, data);
};

export const filterDiscussion = ({ dispatch }, { path, filter }) => {
  dispatch('setLoadingState', true);
  dispatch('fetchDiscussions', { path, filter })
    .then(() => {
      dispatch('setLoadingState', false);
      dispatch('setNotesFetchedState', true);
    })
    .catch(() => {
      dispatch('setLoadingState', false);
      dispatch('setNotesFetchedState', true);
      Flash(__('Something went wrong while fetching comments. Please try again.'));
    });
};

373 374 375 376
export const setCommentsDisabled = ({ commit }, data) => {
  commit(types.DISABLE_COMMENTS, data);
};

377
export const startTaskList = ({ dispatch }) =>
378 379 380 381 382 383 384 385 386
  Vue.nextTick(
    () =>
      new TaskList({
        dataType: 'note',
        fieldName: 'note',
        selector: '.notes .is-editable',
        onSuccess: () => dispatch('startTaskList'),
      }),
  );
387

388 389
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};