gitlabhq/gitlabhq

View on GitHub
app/assets/javascripts/diffs/store/actions.js

Summary

Maintainability
D
2 days
Test Coverage
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/utils/common_utils';
import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
import TreeWorker from '../workers/tree_worker';
import eventHub from '../../notes/event_hub';
import {
  getDiffPositionByLineCode,
  getNoteFormData,
  convertExpandLines,
  idleCallback,
  allDiscussionWrappersExpanded,
} from './utils';
import * as types from './mutation_types';
import {
  PARALLEL_DIFF_VIEW_TYPE,
  INLINE_DIFF_VIEW_TYPE,
  DIFF_VIEW_COOKIE_NAME,
  MR_TREE_SHOW_KEY,
  TREE_LIST_STORAGE_KEY,
  WHITESPACE_STORAGE_KEY,
  TREE_LIST_WIDTH_STORAGE_KEY,
  OLD_LINE_KEY,
  NEW_LINE_KEY,
  TYPE_KEY,
  LEFT_LINE_KEY,
  MAX_RENDERING_DIFF_LINES,
  MAX_RENDERING_BULK_ROWS,
  MIN_RENDERING_MS,
  START_RENDERING_INDEX,
  INLINE_DIFF_LINES_KEY,
  PARALLEL_DIFF_LINES_KEY,
} from '../constants';
import { diffViewerModes } from '~/ide/constants';

export const setBaseConfig = ({ commit }, options) => {
  const { endpoint, projectPath, dismissEndpoint, showSuggestPopover } = options;
  commit(types.SET_BASE_CONFIG, { endpoint, projectPath, dismissEndpoint, showSuggestPopover });
};

export const fetchDiffFiles = ({ state, commit }) => {
  const worker = new TreeWorker();

  commit(types.SET_LOADING, true);

  worker.addEventListener('message', ({ data }) => {
    commit(types.SET_TREE_DATA, data);

    worker.terminate();
  });

  return axios
    .get(mergeUrlParams({ w: state.showWhitespace ? '0' : '1' }, state.endpoint))
    .then(res => {
      commit(types.SET_LOADING, false);
      commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []);
      commit(types.SET_DIFF_DATA, res.data);

      worker.postMessage(state.diffFiles);

      return Vue.nextTick();
    })
    .then(handleLocationHash)
    .catch(() => worker.terminate());
};

export const setHighlightedRow = ({ commit }, lineCode) => {
  const fileHash = lineCode.split('_')[0];
  commit(types.SET_HIGHLIGHTED_ROW, lineCode);
  commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
};

// This is adding line discussions to the actual lines in the diff tree
// once for parallel and once for inline mode
export const assignDiscussionsToDiff = (
  { commit, state, rootState },
  discussions = rootState.notes.discussions,
) => {
  const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
  const hash = getLocationHash();

  discussions
    .filter(discussion => discussion.diff_discussion)
    .forEach(discussion => {
      commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
        discussion,
        diffPositionByLineCode,
        hash,
      });
    });

  Vue.nextTick(() => {
    eventHub.$emit('scrollToDiscussion');
  });
};

export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
  const { file_hash, line_code, id } = removeDiscussion;
  commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash: file_hash, lineCode: line_code, id });
};

export const toggleLineDiscussions = ({ commit }, options) => {
  commit(types.TOGGLE_LINE_DISCUSSIONS, options);
};

export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => {
  const discussion = rootState.notes.discussions.find(d => d.id === discussionId);

  if (discussion) {
    const file = state.diffFiles.find(f => f.file_hash === discussion.diff_file.file_hash);

    if (file) {
      if (!file.renderIt) {
        commit(types.RENDER_FILE, file);
      }

      if (file.viewer.collapsed) {
        eventHub.$emit(`loadCollapsedDiff/${file.file_hash}`);
        scrollToElement(document.getElementById(file.file_hash));
      } else {
        eventHub.$emit('scrollToDiscussion');
      }
    }
  }
};

export const startRenderDiffsQueue = ({ state, commit }) => {
  const checkItem = () =>
    new Promise(resolve => {
      const nextFile = state.diffFiles.find(
        file =>
          !file.renderIt &&
          (file.viewer && (!file.viewer.collapsed || !file.viewer.name === diffViewerModes.text)),
      );

      if (nextFile) {
        requestAnimationFrame(() => {
          commit(types.RENDER_FILE, nextFile);
        });
        requestIdleCallback(
          () => {
            checkItem()
              .then(resolve)
              .catch(() => {});
          },
          { timeout: 1000 },
        );
      } else {
        resolve();
      }
    });

  return checkItem();
};

export const setRenderIt = ({ commit }, file) => commit(types.RENDER_FILE, file);

export const setInlineDiffViewType = ({ commit }) => {
  commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE);

  Cookies.set(DIFF_VIEW_COOKIE_NAME, INLINE_DIFF_VIEW_TYPE);
  const url = mergeUrlParams({ view: INLINE_DIFF_VIEW_TYPE }, window.location.href);
  historyPushState(url);
};

export const setParallelDiffViewType = ({ commit }) => {
  commit(types.SET_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE);

  Cookies.set(DIFF_VIEW_COOKIE_NAME, PARALLEL_DIFF_VIEW_TYPE);
  const url = mergeUrlParams({ view: PARALLEL_DIFF_VIEW_TYPE }, window.location.href);
  historyPushState(url);
};

export const showCommentForm = ({ commit }, { lineCode, fileHash }) => {
  commit(types.TOGGLE_LINE_HAS_FORM, { lineCode, fileHash, hasForm: true });
};

export const cancelCommentForm = ({ commit }, { lineCode, fileHash }) => {
  commit(types.TOGGLE_LINE_HAS_FORM, { lineCode, fileHash, hasForm: false });
};

export const loadMoreLines = ({ commit }, options) => {
  const { endpoint, params, lineNumbers, fileHash } = options;

  params.from_merge_request = true;

  return axios.get(endpoint, { params }).then(res => {
    const contextLines = res.data || [];

    commit(types.ADD_CONTEXT_LINES, {
      lineNumbers,
      contextLines,
      params,
      fileHash,
    });
  });
};

export const scrollToLineIfNeededInline = (_, line) => {
  const hash = getLocationHash();

  if (hash && line.line_code === hash) {
    handleLocationHash();
  }
};

export const scrollToLineIfNeededParallel = (_, line) => {
  const hash = getLocationHash();

  if (
    hash &&
    ((line.left && line.left.line_code === hash) || (line.right && line.right.line_code === hash))
  ) {
    handleLocationHash();
  }
};

export const loadCollapsedDiff = ({ commit, getters, state }, file) =>
  axios
    .get(file.load_collapsed_diff_url, {
      params: {
        commit_id: getters.commitId,
        w: state.showWhitespace ? '0' : '1',
      },
    })
    .then(res => {
      commit(types.ADD_COLLAPSED_DIFFS, {
        file,
        data: res.data,
      });
    });

export const expandAllFiles = ({ commit }) => {
  commit(types.EXPAND_ALL_FILES);
};

/**
 * Toggles the file discussions after user clicked on the toggle discussions button.
 *
 * Gets the discussions for the provided diff.
 *
 * If all discussions are expanded, it will collapse all of them
 * If all discussions are collapsed, it will expand all of them
 * If some discussions are open and others closed, it will expand the closed ones.
 *
 * @param {Object} diff
 */
export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
  const discussions = getters.getDiffFileDiscussions(diff);
  const shouldCloseAll = getters.diffHasAllExpandedDiscussions(diff);
  const shouldExpandAll = getters.diffHasAllCollapsedDiscussions(diff);

  discussions.forEach(discussion => {
    const data = { discussionId: discussion.id };

    if (shouldCloseAll) {
      dispatch('collapseDiscussion', data, { root: true });
    } else if (shouldExpandAll || (!shouldCloseAll && !shouldExpandAll && !discussion.expanded)) {
      dispatch('expandDiscussion', data, { root: true });
    }
  });
};

export const toggleFileDiscussionWrappers = ({ commit }, diff) => {
  const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff);
  let linesWithDiscussions;
  if (diff.highlighted_diff_lines) {
    linesWithDiscussions = diff.highlighted_diff_lines.filter(line => line.discussions.length);
  }
  if (diff.parallel_diff_lines) {
    linesWithDiscussions = diff.parallel_diff_lines.filter(
      line =>
        (line.left && line.left.discussions.length) ||
        (line.right && line.right.discussions.length),
    );
  }

  if (linesWithDiscussions.length) {
    linesWithDiscussions.forEach(line => {
      commit(types.TOGGLE_LINE_DISCUSSIONS, {
        fileHash: diff.file_hash,
        lineCode: line.line_code,
        expanded: !discussionWrappersExpanded,
      });
    });
  }
};

export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
  const postData = getNoteFormData({
    commit: state.commit,
    note,
    ...formData,
  });

  return dispatch('saveNote', postData, { root: true })
    .then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
    .then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
    .then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true }))
    .then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
    .catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};

export const toggleTreeOpen = ({ commit }, path) => {
  commit(types.TOGGLE_FOLDER_OPEN, path);
};

export const scrollToFile = ({ state, commit }, path) => {
  const { fileHash } = state.treeEntries[path];
  document.location.hash = fileHash;

  commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
};

export const toggleShowTreeList = ({ commit, state }, saving = true) => {
  commit(types.TOGGLE_SHOW_TREE_LIST);

  if (saving) {
    localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList);
  }
};

export const openDiffFileCommentForm = ({ commit, getters }, formData) => {
  const form = getters.getCommentFormForDiffFile(formData.fileHash);

  if (form) {
    commit(types.UPDATE_DIFF_FILE_COMMENT_FORM, formData);
  } else {
    commit(types.OPEN_DIFF_FILE_COMMENT_FORM, formData);
  }
};

export const closeDiffFileCommentForm = ({ commit }, fileHash) => {
  commit(types.CLOSE_DIFF_FILE_COMMENT_FORM, fileHash);
};

export const setRenderTreeList = ({ commit }, renderTreeList) => {
  commit(types.SET_RENDER_TREE_LIST, renderTreeList);

  localStorage.setItem(TREE_LIST_STORAGE_KEY, renderTreeList);
};

export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = false }) => {
  commit(types.SET_SHOW_WHITESPACE, showWhitespace);

  localStorage.setItem(WHITESPACE_STORAGE_KEY, showWhitespace);

  if (pushState) {
    historyPushState(mergeUrlParams({ w: showWhitespace ? '0' : '1' }, window.location.href));
  }

  eventHub.$emit('refetchDiffData');
};

export const toggleFileFinder = ({ commit }, visible) => {
  commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible);
};

export const cacheTreeListWidth = (_, size) => {
  localStorage.setItem(TREE_LIST_WIDTH_STORAGE_KEY, size);
};

export const requestFullDiff = ({ commit }, filePath) => commit(types.REQUEST_FULL_DIFF, filePath);
export const receiveFullDiffSucess = ({ commit }, { filePath }) =>
  commit(types.RECEIVE_FULL_DIFF_SUCCESS, { filePath });
export const receiveFullDiffError = ({ commit }, filePath) => {
  commit(types.RECEIVE_FULL_DIFF_ERROR, filePath);
  createFlash(s__('MergeRequest|Error loading full diff. Please try again.'));
};

export const setExpandedDiffLines = ({ commit, state }, { file, data }) => {
  const expandedDiffLines = {
    highlighted_diff_lines: convertExpandLines({
      diffLines: file.highlighted_diff_lines,
      typeKey: TYPE_KEY,
      oldLineKey: OLD_LINE_KEY,
      newLineKey: NEW_LINE_KEY,
      data,
      mapLine: ({ line, oldLine, newLine }) =>
        Object.assign(line, {
          old_line: oldLine,
          new_line: newLine,
          line_code: `${file.file_hash}_${oldLine}_${newLine}`,
        }),
    }),
    parallel_diff_lines: convertExpandLines({
      diffLines: file.parallel_diff_lines,
      typeKey: [LEFT_LINE_KEY, TYPE_KEY],
      oldLineKey: [LEFT_LINE_KEY, OLD_LINE_KEY],
      newLineKey: [LEFT_LINE_KEY, NEW_LINE_KEY],
      data,
      mapLine: ({ line, oldLine, newLine }) => ({
        left: {
          ...line,
          old_line: oldLine,
          line_code: `${file.file_hash}_${oldLine}_${newLine}`,
        },
        right: {
          ...line,
          new_line: newLine,
          line_code: `${file.file_hash}_${newLine}_${oldLine}`,
        },
      }),
    }),
  };
  const currentDiffLinesKey =
    state.diffViewType === INLINE_DIFF_VIEW_TYPE ? INLINE_DIFF_LINES_KEY : PARALLEL_DIFF_LINES_KEY;
  const hiddenDiffLinesKey =
    state.diffViewType === INLINE_DIFF_VIEW_TYPE ? PARALLEL_DIFF_LINES_KEY : INLINE_DIFF_LINES_KEY;

  commit(types.SET_HIDDEN_VIEW_DIFF_FILE_LINES, {
    filePath: file.file_path,
    lines: expandedDiffLines[hiddenDiffLinesKey],
  });

  if (expandedDiffLines[currentDiffLinesKey].length > MAX_RENDERING_DIFF_LINES) {
    let index = START_RENDERING_INDEX;
    commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, {
      filePath: file.file_path,
      lines: expandedDiffLines[currentDiffLinesKey].slice(0, index),
    });
    commit(types.TOGGLE_DIFF_FILE_RENDERING_MORE, file.file_path);

    const idleCb = t => {
      const startIndex = index;

      while (
        t.timeRemaining() >= MIN_RENDERING_MS &&
        index !== expandedDiffLines[currentDiffLinesKey].length &&
        index - startIndex !== MAX_RENDERING_BULK_ROWS
      ) {
        const line = expandedDiffLines[currentDiffLinesKey][index];

        if (line) {
          commit(types.ADD_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: file.file_path, line });
          index += 1;
        }
      }

      if (index !== expandedDiffLines[currentDiffLinesKey].length) {
        idleCallback(idleCb);
      } else {
        commit(types.TOGGLE_DIFF_FILE_RENDERING_MORE, file.file_path);
      }
    };

    idleCallback(idleCb);
  } else {
    commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, {
      filePath: file.file_path,
      lines: expandedDiffLines[currentDiffLinesKey],
    });
  }
};

export const fetchFullDiff = ({ dispatch }, file) =>
  axios
    .get(file.context_lines_path, {
      params: {
        full: true,
        from_merge_request: true,
      },
    })
    .then(({ data }) => {
      dispatch('receiveFullDiffSucess', { filePath: file.file_path });
      dispatch('setExpandedDiffLines', { file, data });
    })
    .catch(() => dispatch('receiveFullDiffError', file.file_path));

export const toggleFullDiff = ({ dispatch, getters, state }, filePath) => {
  const file = state.diffFiles.find(f => f.file_path === filePath);

  dispatch('requestFullDiff', filePath);

  if (file.isShowingFullFile) {
    dispatch('loadCollapsedDiff', file)
      .then(() => dispatch('assignDiscussionsToDiff', getters.getDiffFileDiscussions(file)))
      .catch(() => dispatch('receiveFullDiffError', filePath));
  } else {
    dispatch('fetchFullDiff', file);
  }
};

export const setFileCollapsed = ({ commit }, { filePath, collapsed }) =>
  commit(types.SET_FILE_COLLAPSED, { filePath, collapsed });

export const setSuggestPopoverDismissed = ({ commit, state }) =>
  axios
    .post(state.dismissEndpoint, {
      feature_name: 'suggest_popover_dismissed',
    })
    .then(() => {
      commit(types.SET_SHOW_SUGGEST_POPOVER);
    })
    .catch(() => {
      createFlash(s__('MergeRequest|Error dismissing suggestion popover. Please try again.'));
    });

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