Commit 421acc9a authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into backport-add-epic-sidebar

parents 74b87f02 2932f532
......@@ -416,12 +416,8 @@ ee_compat_check:
- /^[\d-]+-stable(-ee)?/
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
allow_failure: yes
allow_failure: no
retry: 0
cache:
key: "ee_compat_check_repo"
paths:
- ee_compat_check/ee-repo/
artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
when: on_failure
......
......@@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.1.3 (2017-11-10)
- [SECURITY] Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization.
- [FIXED] Fix cancel button not working while uploading on the new issue page. !15137
- [FIXED] Fix webhooks recent deliveries. !15146 (Alexander Randa (@randaalex))
- [FIXED] Fix issues with forked projects of which the source was deleted. !15150
- [FIXED] Fix GPG signature popup info in Safari and Firefox. !15228
- [FIXED] Make sure group and project creation is blocked for new users that are external by default.
- [FIXED] Fix arguments Import/Export error importing project merge requests.
- [FIXED] Fix diff parser so it tolerates to diff special markers in the content.
- [FIXED] Fix a migration that adds merge_requests_ff_only_enabled column to MR table.
- [FIXED] Render 404 when polling commit notes without having permissions.
- [FIXED] Show error message when fast-forward merge is not possible.
- [FIXED] Avoid regenerating the ref path for the environment.
- [PERFORMANCE] Remove Filesystem check metrics that use too much CPU to handle requests.
## 10.1.2 (2017-11-08)
- [SECURITY] Add X-Content-Type-Options header in API responses to make it more difficult to find other vulnerabilities.
......
import { truncate } from './lib/utils/text_utility';
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
......@@ -15,7 +17,7 @@ export default class AbuseReports {
if (reportMessage.length > MAX_MESSAGE_LENGTH) {
$messageCellElement.data('original-message', reportMessage);
$messageCellElement.data('message-truncated', 'true');
$messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
$messageCellElement.text(truncate(reportMessage, MAX_MESSAGE_LENGTH));
}
}
......
......@@ -3,6 +3,7 @@
import Vue from 'vue';
import Flash from '../../../flash';
import './lists_dropdown';
import { pluralize } from '../../../lib/utils/text_utility';
const ModalStore = gl.issueBoards.ModalStore;
......@@ -21,7 +22,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
submitText() {
const count = ModalStore.selectedCount();
return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
return `Add ${count > 0 ? count : ''} ${pluralize('issue', count)}`;
},
},
methods: {
......
......@@ -3,6 +3,8 @@
prefer-template, object-shorthand, prefer-arrow-callback */
/* global Pager */
import { pluralize } from './lib/utils/text_utility';
export default (function () {
const CommitsList = {};
......@@ -86,7 +88,7 @@ export default (function () {
// Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
$commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${gl.text.pluralize('commit', commitsCount)}`);
$commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`);
}
gl.utils.localTimeAgo($processedData.find('.js-timeago'));
......
/* eslint-disable func-names, prefer-arrow-callback */
import Api from './api';
import { humanize } from './lib/utils/text_utility';
export default class CreateLabelDropdown {
constructor($el, namespacePath, projectPath) {
......@@ -107,7 +108,7 @@ export default class CreateLabelDropdown {
errors = label.message;
} else {
errors = Object.keys(label.message).map(key =>
`${gl.text.humanize(key)} ${label.message[key].join(', ')}`,
`${humanize(key)} ${label.message[key].join(', ')}`,
).join('<br/>');
}
......
/* eslint-disable no-param-reassign */
import { __ } from '../locale';
import '../lib/utils/text_utility';
import { dasherize } from '../lib/utils/text_utility';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const EMPTY_STAGE_TEXTS = {
......@@ -36,7 +36,7 @@ export default {
});
newData.stages.forEach((item) => {
const stageSlug = gl.text.dasherize(item.name.toLowerCase());
const stageSlug = dasherize(item.name.toLowerCase());
item.active = false;
item.isUserAllowed = data.permissions[stageSlug];
item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
......
......@@ -2,7 +2,7 @@
import Timeago from 'timeago.js';
import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import '../../lib/utils/text_utility';
import { humanize } from '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
......@@ -139,7 +139,7 @@ export default {
if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map((action) => {
const parsedAction = {
name: gl.text.humanize(action.name),
name: humanize(action.name),
play_path: action.play_path,
playable: action.playable,
};
......
......@@ -338,7 +338,8 @@ class GfmAutoComplete {
let resultantValue = value;
if (value && !this.setting.skipSpecialCharacterTest) {
const withoutAt = value.substring(1);
if (withoutAt && /[^\w\d]/.test(withoutAt)) {
const regex = value.charAt() === '~' ? /\W|^\d+$/ : /\W/;
if (withoutAt && regex.test(withoutAt)) {
resultantValue = `${value.charAt()}"${withoutAt}"`;
}
}
......
......@@ -331,7 +331,7 @@ GitLabDropdown = (function() {
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
return $(selector);
return $(selector, this.instance.dropdown);
};
})(this),
data: (function(_this) {
......
......@@ -2,6 +2,7 @@
import GfmAutoComplete from './gfm_auto_complete';
import dropzoneInput from './dropzone_input';
import textUtils from './lib/utils/text_markdown';
export default class GLForm {
constructor(form, enableGFM = false) {
......@@ -46,7 +47,7 @@ export default class GLForm {
}
// form and textarea event listeners
this.addEventListeners();
gl.text.init(this.form);
textUtils.init(this.form);
// hide discard button
this.form.find('.js-note-discard').hide();
this.form.show();
......@@ -85,7 +86,7 @@ export default class GLForm {
clearEventListeners() {
this.textarea.off('focus');
this.textarea.off('blur');
gl.text.removeListeners(this.form);
textUtils.removeListeners(this.form);
}
addEventListeners() {
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
import 'vendor/jquery.waitforimages';
import '~/lib/utils/text_utility';
import { addDelimiter } from './lib/utils/text_utility';
import Flash from './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
......@@ -73,7 +73,7 @@ export default class Issue {
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
projectIssuesCounter.text(addDelimiter(numProjectIssues));
if (this.createMergeRequestDropdown) {
if (isClosed) {
......
......@@ -172,7 +172,6 @@ export const getSelectedFragment = () => {
return documentFragment;
};
// TODO: Update this name, there is a gl.text.insertText function.
export const insertText = (target, text) => {
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
const selectionStart = target.selectionStart;
......
......@@ -2,6 +2,7 @@
import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format';
import { pluralize } from './text_utility';
import {
lang,
......@@ -142,9 +143,9 @@ export function timeIntervalInWords(intervalInSeconds) {
let text = '';
if (minutes >= 1) {
text = `${minutes} ${gl.text.pluralize('minute', minutes)} ${seconds} ${gl.text.pluralize('second', seconds)}`;
text = `${minutes} ${pluralize('minute', minutes)} ${seconds} ${pluralize('second', seconds)}`;
} else {
text = `${seconds} ${gl.text.pluralize('second', seconds)}`;
text = `${seconds} ${pluralize('second', seconds)}`;
}
return text;
}
......
/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
const textUtils = {};
textUtils.selectedText = function(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
};
textUtils.lineBefore = function(text, textarea) {
var split;
split = text.substring(0, textarea.selectionStart).trim().split('\n');
return split[split.length - 1];
};
textUtils.lineAfter = function(text, textarea) {
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
};
textUtils.blockTagText = function(text, textArea, blockTag, selected) {
var lineAfter, lineBefore;
lineBefore = this.lineBefore(text, textArea);
lineAfter = this.lineAfter(text, textArea);
if (lineBefore === blockTag && lineAfter === blockTag) {
// To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
}
return selected;
} else {
return blockTag + "\n" + selected + "\n" + blockTag;
}
};
textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false;
removedFirstNewLine = false;
currentLineEmpty = false;
// Remove the first newline
if (selected.indexOf('\n') === 0) {
removedFirstNewLine = true;
selected = selected.replace(/\n+/, '');
}
// Remove the last newline
if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
removedLastNewLine = true;
selected = selected.replace(/\n$/, '');
}
selectedSplit = selected.split('\n');
if (!wrap) {
lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
// Check whether the current line is empty or consists only of spaces(=handle as empty)
if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
currentLineEmpty = true;
}
}
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
insertText = this.blockTagText(text, textArea, blockTag, selected);
} else {
insertText = selectedSplit.map(function(val) {
if (val.indexOf(tag) === 0) {
return "" + (val.replace(tag, ''));
} else {
return "" + tag + val;
}
}).join('\n');
}
} else {
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
if (removedFirstNewLine) {
insertText = '\n' + insertText;
}
if (removedLastNewLine) {
insertText += '\n';
}
if (document.queryCommandSupported('insertText')) {
inserted = document.execCommand('insertText', false, insertText);
}
if (!inserted) {
try {
document.execCommand("ms-beginUndoUnit");
} catch (error) {}
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
try {
document.execCommand("ms-endUndoUnit");
} catch (error) {}
}
return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
};
textUtils.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
var pos;
if (!textArea.setSelectionRange) {
return;
}
if (textArea.selectionStart === textArea.selectionEnd) {
if (wrapped) {
pos = textArea.selectionStart - tag.length;
} else {
pos = textArea.selectionStart;
}
if (removedLastNewLine) {
pos -= 1;
}
return textArea.setSelectionRange(pos, pos);
}
};
textUtils.updateText = function(textArea, tag, blockTag, wrap) {
var $textArea, selected, text;
$textArea = $(textArea);
textArea = $textArea.get(0);
text = $textArea.val();
selected = this.selectedText(text, textArea);
$textArea.focus();
return this.insertText(textArea, text, tag, blockTag, selected, wrap);
};
textUtils.init = function(form) {
var self;
self = this;
return $('.js-md', form).off('click').on('click', function() {
var $this;
$this = $(this);
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
});
};
textUtils.removeListeners = function(form) {
return $('.js-md', form).off('click');
};
textUtils.replaceRange = function(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end);
};
export default textUtils;
/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
import 'vendor/latinise';
var base;
var w = window;
if (w.gl == null) {
w.gl = {};
}
if ((base = w.gl).text == null) {
base.text = {};
}
gl.text.addDelimiter = function(text) {
return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
};
/**
* Adds a , to a string composed by numbers, at every 3 chars.
*
* 2333 -> 2,333
* 232324 -> 232,324
*
* @param {String} text
* @returns {String}
*/
export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
/**
* Returns '99+' for numbers bigger than 99.
......@@ -20,182 +15,53 @@ gl.text.addDelimiter = function(text) {
* @param {Number} count
* @return {Number|String}
*/
export function highCountTrim(count) {
return count > 99 ? '99+' : count;
}
export const highCountTrim = count => (count > 99 ? '99+' : count);
/**
* Capitalizes first character
*
* @param {String} text
* @return {String}
*/
export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
gl.text.randomString = function() {
return Math.random().toString(36).substring(7);
};
gl.text.replaceRange = function(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end);
};
gl.text.getTextWidth = function(text, font) {
/**
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
*
* @param {String} text The text to be rendered.
* @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
*
* @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
*/
// re-use canvas object for better performance
var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
var context = canvas.getContext('2d');
context.font = font;
return context.measureText(text).width;
};
gl.text.selectedText = function(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
};
gl.text.lineBefore = function(text, textarea) {
var split;
split = text.substring(0, textarea.selectionStart).trim().split('\n');
return split[split.length - 1];
};
gl.text.lineAfter = function(text, textarea) {
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
};
gl.text.blockTagText = function(text, textArea, blockTag, selected) {
var lineAfter, lineBefore;
lineBefore = this.lineBefore(text, textArea);
lineAfter = this.lineAfter(text, textArea);
if (lineBefore === blockTag && lineAfter === blockTag) {
// To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
}
return selected;
} else {
return blockTag + "\n" + selected + "\n" + blockTag;
}
};
gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false;
removedFirstNewLine = false;
currentLineEmpty = false;
// Remove the first newline
if (selected.indexOf('\n') === 0) {
removedFirstNewLine = true;
selected = selected.replace(/\n+/, '');
}
// Remove the last newline
if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
removedLastNewLine = true;
selected = selected.replace(/\n$/, '');
}
selectedSplit = selected.split('\n');
if (!wrap) {
lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
// Check whether the current line is empty or consists only of spaces(=handle as empty)
if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
currentLineEmpty = true;
}
}
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
insertText = this.blockTagText(text, textArea, blockTag, selected);
} else {
insertText = selectedSplit.map(function(val) {
if (val.indexOf(tag) === 0) {
return "" + (val.replace(tag, ''));
} else {
return "" + tag + val;
}
}).join('\n');
}
} else {
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
/**
* Converst first char to uppercase and replaces undercores with spaces
* @param {String} string
* @requires {String}
*/
export const humanize = string => string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
if (removedFirstNewLine) {
insertText = '\n' + insertText;
}
/**
* Adds an 's' to the end of the string when count is bigger than 0
* @param {String} str
* @param {Number} count
* @returns {String}
*/
export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' : '');
if (removedLastNewLine) {
insertText += '\n';
}
/**
* Replaces underscores with dashes
* @param {*} str
* @returns {String}
*/
export const dasherize = str => str.replace(/[_\s]+/g, '-');
if (document.queryCommandSupported('insertText')) {
inserted = document.execCommand('insertText', false, insertText);
}
if (!inserted) {
try {
document.execCommand("ms-beginUndoUnit");
} catch (error) {}
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
try {
document.execCommand("ms-endUndoUnit");
} catch (error) {}
}
return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
};
gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
var pos;
if (!textArea.setSelectionRange) {
return;
}
if (textArea.selectionStart === textArea.selectionEnd) {
if (wrapped) {
pos = textArea.selectionStart - tag.length;
} else {
pos = textArea.selectionStart;
}
/**
* Removes accents and converts to lower case
* @param {String} str
* @returns {String}
*/
export const slugify = str => str.trim().toLowerCase();
if (removedLastNewLine) {
pos -= 1;
}
/**
* Truncates given text
*
* @param {String} string
* @param {Number} maxLength
* @returns {String}
*/
export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
return textArea.setSelectionRange(pos, pos);
}
};
gl.text.updateText = function(textArea, tag, blockTag, wrap) {
var $textArea, selected, text;
$textArea = $(textArea);
textArea = $textArea.get(0);
text = $textArea.val();
selected = this.selectedText(text, textArea);
$textArea.focus();
return this.insertText(textArea, text, tag, blockTag, selected, wrap);
};
gl.text.init = function(form) {
var self;
self = this;
return $('.js-md', form).off('click').on('click', function() {
var $this;
$this = $(this);
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
});
};
gl.text.removeListeners = function(form) {
return $('.js-md', form).off('click');
};
gl.text.humanize = function(string) {
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
};
gl.text.pluralize = function(str, count) {
return str + (count > 1 || count === 0 ? 's' : '');
};
gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...';
};
gl.text.dasherize = function(str) {
return str.replace(/[_\s]+/g, '-');
};
gl.text.slugify = function(str) {
return str.trim().toLowerCase().latinise();
};
......@@ -30,7 +30,6 @@ import './commit/image_file';
import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility';
import './lib/utils/pretty_time';
import './lib/utils/text_utility';
import './lib/utils/url_utility';
// behaviors
......
......@@ -5,6 +5,7 @@ import 'vendor/jquery.waitforimages';
import TaskList from './task_list';
import './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper';
import { addDelimiter } from './lib/utils/text_utility';
(function() {
this.MergeRequest = (function() {
......@@ -124,7 +125,7 @@ import IssuablesHelper from './helpers/issuables_helper';
const $el = $('.nav-links .js-merge-counter');
const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0);
$el.text(gl.text.addDelimiter(count));
$el.text(addDelimiter(count));
};
MergeRequest.prototype.hideCloseButton = function() {
......
<script>
import tooltip from '../../../vue_shared/directives/tooltip';
import icon from '../../../vue_shared/components/icon.vue';
import { dasherize } from '../../../lib/utils/text_utility';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead.
......@@ -39,7 +39,7 @@
computed: {
cssClass() {
const actionIconDash = gl.text.dasherize(this.actionIcon);
const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`;
},
},
......
......@@ -3,7 +3,7 @@ import flash from '../../flash';
import service from '../services';
import * as types from './mutation_types';
export const redirectToUrl = url => gl.utils.visitUrl(url);
export const redirectToUrl = (_, url) => gl.utils.visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
......@@ -84,7 +84,7 @@ export const commitChanges = ({ commit, state, dispatch, getters }, { payload, n
flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
if (newMr) {
redirectToUrl(`${state.endpoints.newMergeRequestUrl}${branch}`);
dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`);
} else {
commit(types.SET_COMMIT_REF, data.id);
......
......@@ -3,16 +3,16 @@ import * as types from '../mutation_types';
import { pushState } from '../utils';
// eslint-disable-next-line import/prefer-default-export
export const createNewBranch = ({ rootState, commit }, branch) => service.createBranch(
rootState.project.id,
export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
state.project.id,
{
branch,
ref: rootState.currentBranch,
ref: state.currentBranch,
},
).then(res => res.json())
.then((data) => {
const branchName = data.name;
const url = location.href.replace(rootState.currentBranch, branchName);
const url = location.href.replace(state.currentBranch, branchName);
pushState(url);
......
import tooltip from '../../vue_shared/directives/tooltip';
import '../../lib/utils/text_utility';
import { pluralize } from '../../lib/utils/text_utility';
export default {
name: 'MRWidgetHeader',
......@@ -14,7 +14,7 @@ export default {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
return gl.text.pluralize('commit', this.mr.divergedCommitsCount);
return pluralize('commit', this.mr.divergedCommitsCount);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
......
import bp from './breakpoints';
import { slugify } from './lib/utils/text_utility';
export default class Wikis {
constructor() {
......@@ -23,7 +24,7 @@ export default class Wikis {
if (!this.newWikiForm) return;
const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
const slug = gl.text.slugify(slugInput.value);
const slug = slugify(slugInput.value);
if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path');
......
......@@ -45,7 +45,7 @@ class AutocompleteUsersFinder
def find_users
return users_from_project if project
return group.users if group
return group.users_with_parents if group
return User.all if current_user
User.none
......
......@@ -86,6 +86,14 @@ module Milestoneish
false
end
def total_issue_time_spent
@total_issue_time_spent ||= issues.joins(:timelogs).sum(:time_spent)
end
def human_total_issue_time_spent
Gitlab::TimeTrackingFormatter.output(total_issue_time_spent)
end
private
def count_issues_by_state(user)
......
......@@ -262,10 +262,6 @@ class Issue < ActiveRecord::Base
true
end
def update_project_counter_caches?
state_changed? || confidential_changed?
end
def update_project_counter_caches
Projects::OpenIssuesCountService.new(project).refresh_cache
end
......
......@@ -958,10 +958,6 @@ class MergeRequest < ActiveRecord::Base
true
end
def update_project_counter_caches?
state_changed?
end
def update_project_counter_caches
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end
......
......@@ -69,6 +69,10 @@ class PagesDomain < ActiveRecord::Base
current < x509.not_before || x509.not_after < current
end
def expiration
x509&.not_after
end
def subject
return unless x509
x509.subject.to_s
......
module Ci
class PipelineTriggerService < BaseService
include Gitlab::Utils::StrongMemoize
def execute
if trigger_from_token
create_pipeline_from_trigger(trigger_from_token)
......@@ -26,9 +28,9 @@ module Ci
end
def trigger_from_token
return @trigger if defined?(@trigger)
@trigger = Ci::Trigger.find_by_token(params[:token].to_s)
strong_memoize(:trigger) do
Ci::Trigger.find_by_token(params[:token].to_s)
end
end
def create_pipeline_variables!(pipeline)
......
......@@ -187,7 +187,7 @@ class IssuableBaseService < BaseService
# We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save.
update_project_counters = issuable.project && issuable.update_project_counter_caches?
update_project_counters = issuable.project && update_project_counter_caches?(issuable)
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
......@@ -288,4 +288,8 @@ class IssuableBaseService < BaseService
# override if needed
def execute_hooks(issuable, action = 'open', params = {})
end
def update_project_counter_caches?(issuable)
issuable.state_changed?
end
end
......@@ -45,5 +45,9 @@ module Issues
params.delete(:assignee_ids)
end
end
def update_project_counter_caches?(issue)
super || issue.confidential_changed?
end
end
end
......@@ -64,7 +64,7 @@
%th Projects
%th Jobs
%th Tags
%th Last contact
%th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc'))
%th
- @runners.each do |runner|
......
- auth_app_owner = @pre_auth.client.application.owner
%main{ :role => "main" }
.modal-no-backdrop.modal-doorkeepr-auth
.modal-content
......@@ -20,9 +18,14 @@
%p
An application called
= link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer'
is requesting access to your GitLab account. This application was created by
= succeed "." do
= link_to auth_app_owner.name, user_path(auth_app_owner)
is requesting access to your GitLab account.
- auth_app_owner = @pre_auth.client.application.owner
- if auth_app_owner
This application was created by
= succeed "." do
= link_to auth_app_owner.name, user_path(auth_app_owner)
Please note that this application is not provided by GitLab and you should verify its authenticity before
allowing access.
- if @pre_auth.scopes
......
- discussion = @note.discussion if @note.part_of_discussion?
- diff_discussion = discussion&.diff_discussion?
- on_image = discussion.on_image? if diff_discussion
- if discussion
- phrase_end_char = on_image ? "." : ":"
%p.details
= succeed ':' do
= succeed phrase_end_char do
= link_to @note.author_name, user_url(@note.author)
- if discussion.diff_discussion?
- if diff_discussion
- if discussion.new_discussion?
started a new discussion
- else
......@@ -21,7 +26,7 @@
%p.details
#{link_to @note.author_name, user_url(@note.author)} commented:
- if discussion&.diff_discussion?
- if diff_discussion && !on_image
= content_for :head do
= stylesheet_link_tag 'mailers/highlighted_diff_email'
......
......@@ -2,7 +2,7 @@
.create_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-create wide',
dropdown_class: 'dropdown-menu-selectable',
dropdown_class: 'dropdown-menu-selectable capitalize-header',
data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }})
= render 'projects/protected_tags/shared/create_protected_tag'
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></svg>
......@@ -85,6 +85,22 @@
Closed:
= milestone.issues_visible_to_user(current_user).closed.count
.block.time_spent
.sidebar-collapsed-icon
= custom_icon('icon_hourglass')
%span.collapsed-milestone-total-time-spent
- if milestone.human_total_issue_time_spent
= milestone.human_total_issue_time_spent
- else
= _("None")
.title.hide-collapsed
= _("Total issue time spent")
.value.hide-collapsed
- if milestone.human_total_issue_time_spent
%span.bold= milestone.human_total_issue_time_spent
- else
%span.no-value= _("No time spent")
.block.merge-requests
.sidebar-collapsed-icon
%strong
......
---
title: Prevent OAuth phishing attack by presenting detailed wording about app to user
during authorization
merge_request:
author:
type: security
---
title: Fix errors when selecting numeric-only labels in the labels autocomplete selector
merge_request: 14607
author: haseebeqx
type: fixed
---
title: Fix GPG signature popup info in Safari and Firefox
merge_request: 15228
author:
type: fixed
---
title: Add total time spent to milestones
merge_request: 15116
author: George Andrinopoulos
type: added
---
title: Add administrative endpoint to list all pages domains
merge_request: 15160
author: Travis Miller
type: added
---
title: Move update_project_counter_caches? out of issue and merge request
merge_request: 15300
author: George Andrinopoulos
type: other
---
title: Fix webhooks recent deliveries
merge_request: 15146
author: Alexander Randa (@randaalex)
type: fixed
---
title: Revert a regression on runners sorting (!15134)
merge_request: 15341
author: Takuya Noguchi
type: fixed
---
title: Fix issues with forked projects of which the source was deleted
merge_request: 15150
author:
type: fixed
---
title: Prevent error when authorizing an admin-created OAauth application without
a set owner
merge_request:
author:
type: fixed
---
title: Make sure group and project creation is blocked for new users that are external
by default
merge_request:
author:
type: fixed
---
title: Fix arguments Import/Export error importing project merge requests
merge_request:
author:
type: fixed
---
title: Avoid regenerating the ref path for the environment
title: Fix user autocomplete in subgroups
merge_request:
author:
type: fixed
---
title: Fix diff parser so it tolerates to diff special markers in the content
merge_request:
author:
type: fixed
---
title: Fix a migration that adds merge_requests_ff_only_enabled column to MR table
merge_request:
author:
type: fixed
---
title: Render 404 when polling commit notes without having permissions
merge_request:
author:
type: fixed
---
title: Show error message when fast-forward merge is not possible
title: Fix image diff notification email from showing wrong content
merge_request:
author:
type: fixed
---
title: Fix cancel button not working while uploading on the new issue page
merge_request: 15137
author:
type: fixed
---
title: Remove Filesystem check metrics that use too much CPU to handle requests
merge_request:
author:
type: performance
---
title: Export text utils functions as es6 module and add tests
merge_request:
author:
type: other
......@@ -4,6 +4,31 @@ Endpoints for connecting custom domain(s) and TLS certificates in [GitLab Pages]
The GitLab Pages feature must be enabled to use these endpoints. Find out more about [administering](../administration/pages/index.md) and [using](../user/project/pages/index.md) the feature.
## List all pages domains
Get a list of all pages domains. The user must have admin permissions.
```http
GET /pages/domains
```
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/pages/domains
```
```json
[
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
"certificate": {
"expired": false,
"expiration": "2020-04-12T14:32:00.000Z"
}
}
]
```
## List pages domains
Get a list of project pages domains. The user must have permissions to view pages domains.
......
......@@ -490,6 +490,41 @@ Remove all previously JIRA settings from a project.
DELETE /projects/:id/services/jira
```
## Kubernetes
Kubernetes / Openshift integration
### Create/Edit Kubernetes service
Set Kubernetes service for a project.
```
PUT /projects/:id/services/kubernetes
```
Parameters:
- `namespace` (**required**) - The Kubernetes namespace to use
- `api_url` (**required**) - The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com
- `token` (**required**) - The service token to authenticate against the Kubernetes cluster with
- `ca_pem` (optional) - A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)
### Delete Kubernetes service
Delete Kubernetes service for a project.
```
DELETE /projects/:id/services/kubernetes
```
### Get Kubernetes service settings
Get Kubernetes service settings for a project.
```
GET /projects/:id/services/kubernetes
```
## Slack slash commands
Ability to receive slash commands from a Slack chat instance.
......
......@@ -21,7 +21,7 @@ want to add.
---
Select the user and the [permission level](../../user/permissions.md)
Select the user and the [permission level](../../permissions.md)
that you'd like to give the user. Note that you can select more than one user.
![Give user permissions](img/add_user_give_permissions.png)
......
......@@ -42,6 +42,8 @@ module API
# Helper Methods for Grape Endpoint
module HelperMethods
include Gitlab::Utils::StrongMemoize
def find_current_user!
user = find_user_from_access_token || find_user_from_warden
return unless user
......@@ -52,9 +54,9 @@ module API
end
def access_token
return @access_token if defined?(@access_token)
@access_token = find_oauth_access_token || find_personal_access_token
strong_memoize(:access_token) do
find_oauth_access_token || find_personal_access_token
end
end
def validate_access_token!(scopes: [])
......
......@@ -1042,6 +1042,11 @@ module API
expose :value
end
class PagesDomainCertificateExpiration < Grape::Entity
expose :expired?, as: :expired
expose :expiration
end
class PagesDomainCertificate < Grape::Entity
expose :subject
expose :expired?, as: :expired
......@@ -1049,12 +1054,23 @@ module API
expose :certificate_text
end
class PagesDomainBasic < Grape::Entity
expose :domain
expose :url
expose :certificate,
as: :certificate_expiration,
if: ->(pages_domain, _) { pages_domain.certificate? },
using: PagesDomainCertificateExpiration do |pages_domain|
pages_domain
end
end
class PagesDomain < Grape::Entity
expose :domain
expose :url
expose :certificate,
if: ->(pages_domain, _) { pages_domain.certificate? },
using: PagesDomainCertificate do |pages_domain|
if: ->(pages_domain, _) { pages_domain.certificate? },
using: PagesDomainCertificate do |pages_domain|
pages_domain
end
end
......
......@@ -155,6 +155,11 @@ module API
end
end
def authenticated_with_full_private_access!
authenticate!
forbidden! unless current_user.full_private_access?
end
def authenticated_as_admin!
authenticate!
forbidden! unless current_user.admin?
......@@ -190,6 +195,10 @@ module API
not_found! unless user_project.pages_available?
end
def require_pages_config_enabled!
not_found! unless Gitlab.config.pages.enabled
end
def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject)
end
......
......@@ -4,7 +4,6 @@ module API
before do
authenticate!
require_pages_enabled!
end
after_validation do
......@@ -29,10 +28,31 @@ module API
end
end
resource :pages do
before do
require_pages_config_enabled!
authenticated_with_full_private_access!
end
desc "Get all pages domains" do
success Entities::PagesDomainBasic
end
params do
use :pagination
end
get "domains" do
present paginate(PagesDomain.all), with: Entities::PagesDomainBasic
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
before do
require_pages_enabled!
end
desc 'Get all pages domains' do
success Entities::PagesDomain
end
......
......@@ -104,7 +104,7 @@ module Gitlab
end
def exists?
Gitlab::GitalyClient.migrate(:repository_exists) do |enabled|
Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
gitaly_repository_client.exists?
else
......
module Gitlab
module Utils
module StrongMemoize
# Instead of writing patterns like this:
#
# def trigger_from_token
# return @trigger if defined?(@trigger)
#
# @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
# end
#
# We could write it like:
#
# def trigger_from_token
# strong_memoize(:trigger) do
# Ci::Trigger.find_by_token(params[:token].to_s)
# end
# end
#
def strong_memoize(name)
ivar_name = "@#{name}"
if instance_variable_defined?(ivar_name)
instance_variable_get(ivar_name)
else
instance_variable_set(ivar_name, yield)
end
end
end
end
end
......@@ -5,7 +5,7 @@ module QA
include Scenario::Actable
def refresh
visit current_path
visit current_url
end
end
end
......
......@@ -218,18 +218,18 @@ feature 'GFM autocomplete', :js do
user_item = find('.atwho-view li', text: user.username)
expect(user_item).to have_content(user.username)
end
end
def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"")
def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"")
item.click
item.click
if should_wrap
expect(note.value).to include("\"#{value}\"")
else
expect(note.value).not_to include("\"#{value}\"")
end
if should_wrap
expect(note.value).to include("\"#{value}\"")
else
expect(note.value).not_to include("\"#{value}\"")
end
end
end
......@@ -67,6 +67,28 @@ feature 'Create New Merge Request', :js do
expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
end
it 'allows filtering multiple dropdowns' do
visit project_new_merge_request_path(project)
first('.js-source-branch').click
input = find('.dropdown-source-branch .dropdown-input-field')
input.click
input.send_keys('orphaned-branch')
find('.dropdown-source-branch .dropdown-content li', match: :first)
source_items = all('.dropdown-source-branch .dropdown-content li')
expect(source_items.count).to eq(1)
first('.js-target-branch').click
find('.dropdown-target-branch .dropdown-content li', match: :first)
target_items = all('.dropdown-target-branch .dropdown-content li')
expect(target_items.count).to be > 1
end
context 'when target project cannot be viewed by the current user' do
it 'does not leak the private project name & namespace' do
private_project = create(:project, :private, :repository)
......
......@@ -65,4 +65,33 @@ feature 'Milestone' do
expect(find('.alert-danger')).to have_content('already being used for another group or project milestone.')
end
end
feature 'Open a milestone' do
scenario 'shows total issue time spent correctly when no time has been logged' do
milestone = create(:milestone, project: project, title: 8.7)
visit project_milestone_path(project, milestone)
page.within('.block.time_spent') do
expect(page).to have_content 'No time spent'
expect(page).to have_content 'None'
end
end
scenario 'shows total issue time spent' do
milestone = create(:milestone, project: project, title: 8.7)
issue1 = create(:issue, project: project, milestone: milestone)
issue2 = create(:issue, project: project, milestone: milestone)
issue1.spend_time(duration: 3600, user: user)
issue1.save!
issue2.spend_time(duration: 7200, user: user)
issue2.save!
visit project_milestone_path(project, milestone)
page.within('.block.time_spent') do
expect(page).to have_content '3h'
end
end
end
end
......@@ -42,6 +42,21 @@ describe AutocompleteUsersFinder do
it { is_expected.to match_array([user1]) }
end
context 'when passed a subgroup', :nested_groups do
let(:grandparent) { create(:group, :public) }
let(:parent) { create(:group, :public, parent: grandparent) }
let(:child) { create(:group, :public, parent: parent) }
let(:group) { parent }
let!(:grandparent_user) { create(:group_member, :developer, group: grandparent).user }
let!(:parent_user) { create(:group_member, :developer, group: parent).user }
let!(:child_user) { create(:group_member, :developer, group: child).user }
it 'includes users from parent groups as well' do
expect(subject).to match_array([grandparent_user, parent_user])
end
end
it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) }
context 'when filtered by search' do
......
{
"type": "object",
"properties": {
"domain": { "type": "string" },
"url": { "type": "uri" },
"certificate_expiration": {
"type": "object",
"properties": {
"expired": { "type": "boolean" },
"expiration": { "type": "string" }
},
"required": ["expired", "expiration"],
"additionalProperties": false
}
},
"required": ["domain", "url"],
"additionalProperties": false
}
{
"type": "object",
"properties": {
"domain": { "type": "string" },
"url": { "type": "uri" },
"certificate": {
"type": "object",
"properties": {
"subject": { "type": "string" },
"expired": { "type": "boolean" },
"certificate": { "type": "string" },
"certificate_text": { "type": "string" }
},
"required": ["subject", "expired"],
"additionalProperties": false
}
},
"required": ["domain", "url"],
"additionalProperties": false
}
{
"type": "array",
"items": { "$ref": "pages_domain/basic.json" }
}
{
"type": "array",
"items": {
"type": "object",
"properties": {
"domain": { "type": "string" },
"url": { "type": "uri" },
"certificate": {
"type": "object",
"properties": {
"subject": { "type": "string" },
"expired": { "type": "boolean" },
"certificate": { "type": "string" },
"certificate_text": { "type": "string" }
},
"required": ["subject", "expired"],
"additionalProperties": false
}
},
"required": ["domain", "url"],
"additionalProperties": false
}
"items": { "$ref": "pages_domain/detail.json" }
}
......@@ -67,6 +67,28 @@ describe('GfmAutoComplete', function () {
});
});
describe('DefaultOptions.beforeInsert', () => {
const beforeInsert = (context, value) => (
gfmAutoCompleteCallbacks.beforeInsert.call(context, value)
);
const atwhoInstance = { setting: { skipSpecialCharacterTest: false } };
it('should not quote if value only contains alphanumeric charecters', () => {
expect(beforeInsert(atwhoInstance, '@user1')).toBe('@user1');
expect(beforeInsert(atwhoInstance, '~label1')).toBe('~label1');
});
it('should quote if value contains any non-alphanumeric characters', () => {
expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label-20"');
expect(beforeInsert(atwhoInstance, '~label 20')).toBe('~"label 20"');
});
it('should quote integer labels', () => {
expect(beforeInsert(atwhoInstance, '~1234')).toBe('~"1234"');
});
});
describe('DefaultOptions.matcher', function () {
const defaultMatcher = (context, flag, subtext) => (
gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext)
......
import textUtils from '~/lib/utils/text_markdown';
describe('init markdown', () => {
let textArea;
beforeAll(() => {
textArea = document.createElement('textarea');
document.querySelector('body').appendChild(textArea);
textArea.focus();
});
afterAll(() => {
textArea.parentNode.removeChild(textArea);
});
describe('without selection', () => {
it('inserts the tag on an empty line', () => {
const initialValue = '';
textArea.value = initialValue;
textArea.selectionStart = 0;
textArea.selectionEnd = 0;
textUtils.insertText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
it('inserts the tag on a new line if the current one is not empty', () => {
const initialValue = 'some text';
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
textUtils.insertText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}\n* `);
});
it('inserts the tag on the same line if the current line only contains spaces', () => {
const initialValue = ' ';
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
textUtils.insertText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
it('inserts the tag on the same line if the current line only contains tabs', () => {
const initialValue = '\t\t\t';
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
textUtils.insertText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
});
});
import * as textUtility from '~/lib/utils/text_utility';
import * as textUtils from '~/lib/utils/text_utility';
describe('text_utility', () => {
describe('gl.text.getTextWidth', () => {
it('returns zero width when no text is passed', () => {
expect(gl.text.getTextWidth('')).toBe(0);
describe('addDelimiter', () => {
it('should add a delimiter to the given string', () => {
expect(textUtils.addDelimiter('1234')).toEqual('1,234');
expect(textUtils.addDelimiter('222222')).toEqual('222,222');
});
it('returns zero width when no text is passed and font is passed', () => {
expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
});
it('returns width when text is passed', () => {
expect(gl.text.getTextWidth('foo') > 0).toBe(true);
});
it('returns bigger width when font is larger', () => {
const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
const regular = gl.text.getTextWidth('foo', '10px sans-serif');
expect(largeFont > regular).toBe(true);
});
});
describe('gl.text.pluralize', () => {
it('returns pluralized', () => {
expect(gl.text.pluralize('test', 2)).toBe('tests');
});
it('returns pluralized when count is 0', () => {
expect(gl.text.pluralize('test', 0)).toBe('tests');
});
it('does not return pluralized', () => {
expect(gl.text.pluralize('test', 1)).toBe('test');
it('should not add a delimiter if string contains no numbers', () => {
expect(textUtils.addDelimiter('aaaa')).toEqual('aaaa');
});
});
describe('highCountTrim', () => {
it('returns 99+ for count >= 100', () => {
expect(textUtility.highCountTrim(105)).toBe('99+');
expect(textUtility.highCountTrim(100)).toBe('99+');
expect(textUtils.highCountTrim(105)).toBe('99+');
expect(textUtils.highCountTrim(100)).toBe('99+');
});
it('returns exact number for count < 100', () => {
expect(textUtility.highCountTrim(45)).toBe(45);
expect(textUtils.highCountTrim(45)).toBe(45);
});
});
describe('capitalizeFirstCharacter', () => {
it('returns string with first letter capitalized', () => {
expect(textUtility.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab');
expect(textUtils.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab');
expect(textUtils.highCountTrim(105)).toBe('99+');
expect(textUtils.highCountTrim(100)).toBe('99+');
});
});
describe('gl.text.insertText', () => {
let textArea;
beforeAll(() => {
textArea = document.createElement('textarea');
document.querySelector('body').appendChild(textArea);
textArea.focus();
describe('humanize', () => {
it('should remove underscores and uppercase the first letter', () => {
expect(textUtils.humanize('foo_bar')).toEqual('Foo bar');
});
});
afterAll(() => {
textArea.parentNode.removeChild(textArea);
describe('pluralize', () => {
it('should pluralize given string', () => {
expect(textUtils.pluralize('test', 2)).toBe('tests');
});
describe('without selection', () => {
it('inserts the tag on an empty line', () => {
const initialValue = '';
textArea.value = initialValue;
textArea.selectionStart = 0;
textArea.selectionEnd = 0;
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
it('inserts the tag on a new line if the current one is not empty', () => {
const initialValue = 'some text';
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}\n* `);
});
it('inserts the tag on the same line if the current line only contains spaces', () => {
const initialValue = ' ';
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
it('inserts the tag on the same line if the current line only contains tabs', () => {
const initialValue = '\t\t\t';
it('should pluralize when count is 0', () => {
expect(textUtils.pluralize('test', 0)).toBe('tests');
});
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
it('should not pluralize when count is 1', () => {
expect(textUtils.pluralize('test', 1)).toBe('test');
});
});
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
describe('dasherize', () => {
it('should replace underscores with dashes', () => {
expect(textUtils.dasherize('foo_bar_foo')).toEqual('foo-bar-foo');
});
});
expect(textArea.value).toEqual(`${initialValue}* `);
});
describe('slugify', () => {
it('should remove accents and convert to lower case', () => {
expect(textUtils.slugify('João')).toEqual('joão');
});
});
});
......@@ -12,9 +12,4 @@ export const file = (name = 'name', id = name, type = '') => decorateData({
url: 'url',
name,
path: name,
last_commit: {
id: '123',
message: 'test',
committed_date: new Date().toISOString(),
},
});
import store from '~/repo/stores';
import service from '~/repo/services';
import { resetStore } from '../../helpers';
describe('Multi-file store branch actions', () => {
afterEach(() => {
resetStore(store);
});
describe('createNewBranch', () => {
beforeEach(() => {
spyOn(service, 'createBranch').and.returnValue(Promise.resolve({
json: () => ({
name: 'testing',
}),
}));
spyOn(history, 'pushState');
store.state.project.id = 2;
store.state.currentBranch = 'testing';
});
it('creates new branch', (done) => {
store.dispatch('createNewBranch', 'master')
.then(() => {
expect(store.state.currentBranch).toBe('testing');
expect(service.createBranch).toHaveBeenCalledWith(2, {
branch: 'master',
ref: 'testing',
});
expect(history.pushState).toHaveBeenCalled();
done();
})
.catch(done.fail);
});
});
});
import Vue from 'vue';
import store from '~/repo/stores';
import service from '~/repo/services';
import { file, resetStore } from '../../helpers';
describe('Multi-file store file actions', () => {
afterEach(() => {
resetStore(store);
});
describe('closeFile', () => {
let localFile;
let getLastCommitDataSpy;
let oldGetLastCommitData;
beforeEach(() => {
getLastCommitDataSpy = jasmine.createSpy('getLastCommitData');
oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line
store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line
localFile = file();
localFile.active = true;
localFile.opened = true;
localFile.parentTreeUrl = 'parentTreeUrl';
store.state.openFiles.push(localFile);
spyOn(history, 'pushState');
});
afterEach(() => {
store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
});
it('closes open files', (done) => {
store.dispatch('closeFile', { file: localFile })
.then(() => {
expect(localFile.opened).toBeFalsy();
expect(localFile.active).toBeFalsy();
expect(store.state.openFiles.length).toBe(0);
done();
}).catch(done.fail);
});
it('does not close file if has changed', (done) => {
localFile.changed = true;
store.dispatch('closeFile', { file: localFile })
.then(() => {
expect(localFile.opened).toBeTruthy();
expect(localFile.active).toBeTruthy();
expect(store.state.openFiles.length).toBe(1);
done();
}).catch(done.fail);
});
it('does not close file if temp file', (done) => {
localFile.tempFile = true;
store.dispatch('closeFile', { file: localFile })
.then(() => {
expect(localFile.opened).toBeTruthy();
expect(localFile.active).toBeTruthy();
expect(store.state.openFiles.length).toBe(1);
done();
}).catch(done.fail);
});
it('force closes a changed file', (done) => {
localFile.changed = true;
store.dispatch('closeFile', { file: localFile, force: true })
.then(() => {
expect(localFile.opened).toBeFalsy();
expect(localFile.active).toBeFalsy();
expect(store.state.openFiles.length).toBe(0);
done();
}).catch(done.fail);
});
it('calls pushState when no open files are left', (done) => {
store.dispatch('closeFile', { file: localFile })
.then(() => {
expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'parentTreeUrl');
done();
}).catch(done.fail);
});
it('sets next file as active', (done) => {
const f = file();
store.state.openFiles.push(f);
expect(f.active).toBeFalsy();
store.dispatch('closeFile', { file: localFile })
.then(() => {
expect(f.active).toBeTruthy();
done();
}).catch(done.fail);
});
it('calls getLastCommitData', (done) => {
store.dispatch('closeFile', { file: localFile })
.then(() => {
expect(getLastCommitDataSpy).toHaveBeenCalled();
done();
}).catch(done.fail);
});
});
describe('setFileActive', () => {
let scrollToTabSpy;
let oldScrollToTab;
beforeEach(() => {
scrollToTabSpy = jasmine.createSpy('scrollToTab');
oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line
store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
});
afterEach(() => {
store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
});
it('calls scrollToTab', (done) => {
store.dispatch('setFileActive', file())
.then(() => {
expect(scrollToTabSpy).toHaveBeenCalled();
done();
}).catch(done.fail);
});
it('sets the file active', (done) => {
const localFile = file();
store.dispatch('setFileActive', localFile)
.then(() => {
expect(localFile.active).toBeTruthy();
done();
}).catch(done.fail);
});
it('returns early if file is already active', (done) => {
const localFile = file();
localFile.active = true;
store.dispatch('setFileActive', localFile)
.then(() => {
expect(scrollToTabSpy).not.toHaveBeenCalled();
done();
}).catch(done.fail);
});
it('sets current active file to not active', (done) => {
const localFile = file();
localFile.active = true;
store.state.openFiles.push(localFile);
store.dispatch('setFileActive', file())
.then(() => {
expect(localFile.active).toBeFalsy();
done();
}).catch(done.fail);
});
it('resets location.hash for line highlighting', (done) => {
location.hash = 'test';
store.dispatch('setFileActive', file())
.then(() => {
expect(location.hash).not.toBe('test');
done();
}).catch(done.fail);
});
});
describe('getFileData', () => {
let localFile = file();
beforeEach(() => {
spyOn(service, 'getFileData').and.returnValue(Promise.resolve({
headers: {
'page-title': 'testing getFileData',
},
json: () => Promise.resolve({
blame_path: 'blame_path',
commits_path: 'commits_path',
permalink: 'permalink',
raw_path: 'raw_path',
binary: false,
html: '123',
render_error: '',
}),
}));
localFile = file();
localFile.url = 'getFileDataURL';
});
it('calls the service', (done) => {
store.dispatch('getFileData', localFile)
.then(() => {
expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
done();
}).catch(done.fail);
});
it('sets the file data', (done) => {
store.dispatch('getFileData', localFile)
.then(Vue.nextTick)
.then(() => {
expect(localFile.blamePath).toBe('blame_path');
done();
}).catch(done.fail);
});
it('sets document title', (done) => {
store.dispatch('getFileData', localFile)
.then(() => {
expect(document.title).toBe('testing getFileData');
done();
}).catch(done.fail);
});
it('sets the file as active', (done) => {
store.dispatch('getFileData', localFile)
.then(Vue.nextTick)
.then(() => {
expect(localFile.active).toBeTruthy();
done();
}).catch(done.fail);
});
it('adds the file to open files', (done) => {
store.dispatch('getFileData', localFile)
.then(Vue.nextTick)
.then(() => {
expect(store.state.openFiles.length).toBe(1);
expect(store.state.openFiles[0].name).toBe(localFile.name);
done();
}).catch(done.fail);
});
it('toggles the file loading', (done) => {
store.dispatch('getFileData', localFile)
.then(() => {
expect(localFile.loading).toBeTruthy();
return Vue.nextTick();
})
.then(() => {
expect(localFile.loading).toBeFalsy();
done();
}).catch(done.fail);
});
});
describe('getRawFileData', () => {
let tmpFile;
beforeEach(() => {
spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw'));
tmpFile = file();
});
it('calls getRawFileData service method', (done) => {
store.dispatch('getRawFileData', tmpFile)
.then(() => {
expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
done();
}).catch(done.fail);
});
it('updates file raw data', (done) => {
store.dispatch('getRawFileData', tmpFile)
.then(() => {
expect(tmpFile.raw).toBe('raw');
done();
}).catch(done.fail);
});
});
describe('changeFileContent', () => {
let tmpFile;
beforeEach(() => {
tmpFile = file();
});
it('updates file content', (done) => {
store.dispatch('changeFileContent', {
file: tmpFile,
content: 'content',
})
.then(() => {
expect(tmpFile.content).toBe('content');
done();
}).catch(done.fail);
});
});
describe('createTempFile', () => {
beforeEach(() => {
document.body.innerHTML += '<div class="flash-container"></div>';
});
afterEach(() => {
document.querySelector('.flash-container').remove();
});
it('creates temp file', (done) => {
store.dispatch('createTempFile', {
tree: store.state,
name: 'test',
}).then((f) => {
expect(f.tempFile).toBeTruthy();
expect(store.state.tree.length).toBe(1);
done();
}).catch(done.fail);
});
it('adds tmp file to open files', (done) => {
store.dispatch('createTempFile', {
tree: store.state,
name: 'test',
}).then((f) => {
expect(store.state.openFiles.length).toBe(1);
expect(store.state.openFiles[0].name).toBe(f.name);
done();
}).catch(done.fail);
});
it('sets tmp file as active', (done) => {
store.dispatch('createTempFile', {
tree: store.state,
name: 'test',
}).then((f) => {
expect(f.active).toBeTruthy();
done();
}).catch(done.fail);
});
it('enters edit mode if file is not base64', (done) => {
store.dispatch('createTempFile', {
tree: store.state,
name: 'test',
}).then(() => {
expect(store.state.editMode).toBeTruthy();
done();
}).catch(done.fail);
});
it('does not enter edit mode if file is base64', (done) => {
store.dispatch('createTempFile', {
tree: store.state,
name: 'test',
base64: true,
}).then(() => {
expect(store.state.editMode).toBeFalsy();
done();
}).catch(done.fail);
});
it('creates flash message is file already exists', (done) => {
store.state.tree.push(file('test', '1', 'blob'));
store.dispatch('createTempFile', {
tree: store.state,
name: 'test',
}).then(() => {
expect(document.querySelector('.flash-alert')).not.toBeNull();
done();
}).catch(done.fail);
});
it('increases level of file', (done) => {
store.state.level = 1;
store.dispatch('createTempFile', {
tree: store.state,
name: 'test',
}).then((f) => {
expect(f.level).toBe(2);
done();
}).catch(done.fail);
});
});
});
import Vue from 'vue';
import store from '~/repo/stores';
import service from '~/repo/services';
import { file, resetStore } from '../../helpers';
describe('Multi-file store tree actions', () => {
afterEach(() => {
resetStore(store);
});
describe('getTreeData', () => {
beforeEach(() => {
spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
headers: {
'page-title': 'test',
},
json: () => Promise.resolve({
last_commit_path: 'last_commit_path',
parent_tree_url: 'parent_tree_url',
path: '/',
trees: [{ name: 'tree' }],
blobs: [{ name: 'blob' }],
submodules: [{ name: 'submodule' }],
}),
}));
spyOn(history, 'pushState');
Object.assign(store.state.endpoints, {
rootEndpoint: 'rootEndpoint',
});
});
it('calls service getTreeData', (done) => {
store.dispatch('getTreeData')
.then(() => {
expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint');
done();
}).catch(done.fail);
});
it('adds data into tree', (done) => {
store.dispatch('getTreeData')
.then(Vue.nextTick)
.then(() => {
expect(store.state.tree.length).toBe(3);
expect(store.state.tree[0].type).toBe('tree');
expect(store.state.tree[1].type).toBe('submodule');
expect(store.state.tree[2].type).toBe('blob');
done();
}).catch(done.fail);
});
it('sets parent tree URL', (done) => {
store.dispatch('getTreeData')
.then(Vue.nextTick)
.then(() => {
expect(store.state.parentTreeUrl).toBe('parent_tree_url');
done();
}).catch(done.fail);
});
it('sets last commit path', (done) => {
store.dispatch('getTreeData')
.then(Vue.nextTick)
.then(() => {
expect(store.state.lastCommitPath).toBe('last_commit_path');
done();
}).catch(done.fail);
});
it('sets root if not currently at root', (done) => {
store.state.isInitialRoot = false;
store.dispatch('getTreeData')
.then(Vue.nextTick)
.then(() => {
expect(store.state.isInitialRoot).toBeTruthy();
expect(store.state.isRoot).toBeTruthy();
done();
}).catch(done.fail);
});
it('sets page title', (done) => {
store.dispatch('getTreeData')
.then(() => {
expect(document.title).toBe('test');
done();
}).catch(done.fail);
});
it('toggles loading', (done) => {
store.dispatch('getTreeData')
.then(() => {
expect(store.state.loading).toBeTruthy();
return Vue.nextTick();
})
.then(() => {
expect(store.state.loading).toBeFalsy();
done();
}).catch(done.fail);
});
it('calls pushState with endpoint', (done) => {
store.dispatch('getTreeData')
.then(Vue.nextTick)
.then(() => {
expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'rootEndpoint');
done();
}).catch(done.fail);
});
it('calls getLastCommitData if prevLastCommitPath is not null', (done) => {
const getLastCommitDataSpy = jasmine.createSpy('getLastCommitData');
const oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line
store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line
store.state.prevLastCommitPath = 'test';
store.dispatch('getTreeData')
.then(Vue.nextTick)
.then(() => {
expect(getLastCommitDataSpy).toHaveBeenCalledWith(store.state);
store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
done();
}).catch(done.fail);
});
});
describe('toggleTreeOpen', () => {
let oldGetTreeData;
let getTreeDataSpy;
let tree;
beforeEach(() => {
getTreeDataSpy = jasmine.createSpy('getTreeData');
oldGetTreeData = store._actions.getTreeData; // eslint-disable-line
store._actions.getTreeData = [getTreeDataSpy]; // eslint-disable-line
tree = {
opened: false,
tree: [],
};
});
afterEach(() => {
store._actions.getTreeData = oldGetTreeData; // eslint-disable-line
});
it('toggles the tree open', (done) => {
store.dispatch('toggleTreeOpen', {
endpoint: 'test',
tree,
}).then(() => {
expect(tree.opened).toBeTruthy();
done();
}).catch(done.fail);
});
it('calls getTreeData if tree is closed', (done) => {
store.dispatch('toggleTreeOpen', {
endpoint: 'test',
tree,
}).then(() => {
expect(getTreeDataSpy).toHaveBeenCalledWith({
endpoint: 'test',
tree,
});
expect(store.state.previousUrl).toBe('test');
done();
}).catch(done.fail);
});
it('resets entries tree', (done) => {
Object.assign(tree, {
opened: true,
tree: ['a'],
});
store.dispatch('toggleTreeOpen', {
endpoint: 'test',
tree,
}).then(() => {
expect(tree.tree.length).toBe(0);
done();
}).catch(done.fail);
});
it('pushes new state', (done) => {
spyOn(history, 'pushState');
Object.assign(tree, {
opened: true,
parentTreeUrl: 'testing',
});
store.dispatch('toggleTreeOpen', {
endpoint: 'test',
tree,
}).then(() => {
expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'testing');
done();
}).catch(done.fail);
});
});
describe('clickedTreeRow', () => {
describe('tree', () => {
let toggleTreeOpenSpy;
let oldToggleTreeOpen;
beforeEach(() => {
toggleTreeOpenSpy = jasmine.createSpy('toggleTreeOpen');
oldToggleTreeOpen = store._actions.toggleTreeOpen; // eslint-disable-line
store._actions.toggleTreeOpen = [toggleTreeOpenSpy]; // eslint-disable-line
});
afterEach(() => {
store._actions.toggleTreeOpen = oldToggleTreeOpen; // eslint-disable-line
});
it('opens tree', (done) => {
const tree = {
url: 'a',
type: 'tree',
};
store.dispatch('clickedTreeRow', tree)
.then(() => {
expect(toggleTreeOpenSpy).toHaveBeenCalledWith({
endpoint: tree.url,
tree,
});
done();
}).catch(done.fail);
});
});
describe('submodule', () => {
let row;
beforeEach(() => {
spyOn(gl.utils, 'visitUrl');
row = {
url: 'submoduleurl',
type: 'submodule',
loading: false,
};
});
it('toggles loading for row', (done) => {
store.dispatch('clickedTreeRow', row)
.then(() => {
expect(row.loading).toBeTruthy();
done();
}).catch(done.fail);
});
it('opens submodule URL', (done) => {
store.dispatch('clickedTreeRow', row)
.then(() => {
expect(gl.utils.visitUrl).toHaveBeenCalledWith('submoduleurl');
done();
}).catch(done.fail);
});
});
describe('blob', () => {
let row;
beforeEach(() => {
row = {
type: 'blob',
opened: false,
};
});
it('calls getFileData', (done) => {
const getFileDataSpy = jasmine.createSpy('getFileData');
const oldGetFileData = store._actions.getFileData; // eslint-disable-line
store._actions.getFileData = [getFileDataSpy]; // eslint-disable-line
store.dispatch('clickedTreeRow', row)
.then(() => {
expect(getFileDataSpy).toHaveBeenCalledWith(row);
store._actions.getFileData = oldGetFileData; // eslint-disable-line
done();
}).catch(done.fail);
});
it('calls setFileActive when file is opened', (done) => {
const setFileActiveSpy = jasmine.createSpy('setFileActive');
const oldSetFileActive = store._actions.setFileActive; // eslint-disable-line
store._actions.setFileActive = [setFileActiveSpy]; // eslint-disable-line
row.opened = true;
store.dispatch('clickedTreeRow', row)
.then(() => {
expect(setFileActiveSpy).toHaveBeenCalledWith(row);
store._actions.setFileActive = oldSetFileActive; // eslint-disable-line
done();
}).catch(done.fail);
});
});
});
describe('createTempTree', () => {
it('creates temp tree', (done) => {
store.dispatch('createTempTree', 'test')
.then(() => {
expect(store.state.tree[0].tempFile).toBeTruthy();
expect(store.state.tree[0].name).toBe('test');
expect(store.state.tree[0].type).toBe('tree');
done();
}).catch(done.fail);
});
it('creates .gitkeep file in temp tree', (done) => {
store.dispatch('createTempTree', 'test')
.then(() => {
expect(store.state.tree[0].tree[0].tempFile).toBeTruthy();
expect(store.state.tree[0].tree[0].name).toBe('.gitkeep');
done();
}).catch(done.fail);
});
it('creates new folder inside another tree', (done) => {
const tree = {
type: 'tree',
name: 'testing',
tree: [],
};
store.state.tree.push(tree);
store.dispatch('createTempTree', 'testing/test')
.then(() => {
expect(store.state.tree[0].name).toBe('testing');
expect(store.state.tree[0].tree[0].tempFile).toBeTruthy();
expect(store.state.tree[0].tree[0].name).toBe('test');
expect(store.state.tree[0].tree[0].type).toBe('tree');
done();
}).catch(done.fail);
});
it('does not create new tree if already exists', (done) => {
const tree = {
type: 'tree',
name: 'testing',
tree: [],
};
store.state.tree.push(tree);
store.dispatch('createTempTree', 'testing/test')
.then(() => {
expect(store.state.tree[0].name).toBe('testing');
expect(store.state.tree[0].tempFile).toBeUndefined();
done();
}).catch(done.fail);
});
});
describe('getLastCommitData', () => {
beforeEach(() => {
spyOn(service, 'getTreeLastCommit').and.returnValue(Promise.resolve({
headers: {
'more-logs-url': null,
},
json: () => Promise.resolve([{
type: 'tree',
file_name: 'testing',
commit: {
message: 'commit message',
authored_date: '123',
},
}]),
}));
store.state.tree.push(file('testing', '1', 'tree'));
store.state.lastCommitPath = 'lastcommitpath';
});
it('calls service with lastCommitPath', (done) => {
store.dispatch('getLastCommitData')
.then(() => {
expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
done();
}).catch(done.fail);
});
it('updates trees last commit data', (done) => {
store.dispatch('getLastCommitData')
.then(Vue.nextTick)
.then(() => {
expect(store.state.tree[0].lastCommit.message).toBe('commit message');
done();
}).catch(done.fail);
});
it('does not update entry if not found', (done) => {
store.state.tree[0].name = 'a';
store.dispatch('getLastCommitData')
.then(Vue.nextTick)
.then(() => {
expect(store.state.tree[0].lastCommit.message).not.toBe('commit message');
done();
}).catch(done.fail);
});
});
describe('updateDirectoryData', () => {
it('adds data into tree', (done) => {
const tree = {
tree: [],
};
const data = {
trees: [{ name: 'tree' }],
submodules: [{ name: 'submodule' }],
blobs: [{ name: 'blob' }],
};
store.dispatch('updateDirectoryData', {
data,
tree,
}).then(() => {
expect(tree.tree[0].name).toBe('tree');
expect(tree.tree[0].type).toBe('tree');
expect(tree.tree[1].name).toBe('submodule');
expect(tree.tree[1].type).toBe('submodule');
expect(tree.tree[2].name).toBe('blob');
expect(tree.tree[2].type).toBe('blob');
done();
}).catch(done.fail);
});
});
});
import Vue from 'vue';
import store from '~/repo/stores';
import service from '~/repo/services';
import { resetStore, file } from '../helpers';
describe('Multi-file store actions', () => {
afterEach(() => {
resetStore(store);
});
describe('redirectToUrl', () => {
it('calls visitUrl', (done) => {
spyOn(gl.utils, 'visitUrl');
store.dispatch('redirectToUrl', 'test')
.then(() => {
expect(gl.utils.visitUrl).toHaveBeenCalledWith('test');
done();
})
.catch(done.fail);
});
});
describe('setInitialData', () => {
it('commits initial data', (done) => {
store.dispatch('setInitialData', { canCommit: true })
.then(() => {
expect(store.state.canCommit).toBeTruthy();
done();
})
.catch(done.fail);
});
});
describe('closeDiscardPopup', () => {
it('closes the discard popup', (done) => {
store.dispatch('closeDiscardPopup', false)
.then(() => {
expect(store.state.discardPopupOpen).toBeFalsy();
done();
})
.catch(done.fail);
});
});
describe('discardAllChanges', () => {
beforeEach(() => {
store.state.openFiles.push(file());
store.state.openFiles[0].changed = true;
});
});
describe('closeAllFiles', () => {
beforeEach(() => {
store.state.openFiles.push(file());
store.state.openFiles[0].opened = true;
});
it('closes all open files', (done) => {
store.dispatch('closeAllFiles')
.then(() => {
expect(store.state.openFiles.length).toBe(0);
done();
})
.catch(done.fail);
});
});
describe('toggleEditMode', () => {
it('toggles edit mode', (done) => {
store.state.editMode = true;
store.dispatch('toggleEditMode')
.then(() => {
expect(store.state.editMode).toBeFalsy();
done();
}).catch(done.fail);
});
it('sets preview mode', (done) => {
store.state.currentBlobView = 'repo-editor';
store.state.editMode = true;
store.dispatch('toggleEditMode')
.then(Vue.nextTick)
.then(() => {
expect(store.state.currentBlobView).toBe('repo-preview');
done();
}).catch(done.fail);
});
it('opens discard popup if there are changed files', (done) => {
store.state.editMode = true;
store.state.openFiles.push(file());
store.state.openFiles[0].changed = true;
store.dispatch('toggleEditMode')
.then(() => {
expect(store.state.discardPopupOpen).toBeTruthy();
done();
}).catch(done.fail);
});
it('can force closed if there are changed files', (done) => {
store.state.editMode = true;
store.state.openFiles.push(file());
store.state.openFiles[0].changed = true;
store.dispatch('toggleEditMode', true)
.then(() => {
expect(store.state.discardPopupOpen).toBeFalsy();
expect(store.state.editMode).toBeFalsy();
done();
}).catch(done.fail);
});
it('discards file changes', (done) => {
const f = file();
store.state.editMode = true;
store.state.tree.push(f);
store.state.openFiles.push(f);
f.changed = true;
store.dispatch('toggleEditMode', true)
.then(Vue.nextTick)
.then(() => {
expect(f.changed).toBeFalsy();
done();
}).catch(done.fail);
});
});
describe('toggleBlobView', () => {
it('sets edit mode view if in edit mode', (done) => {
store.state.editMode = true;
store.dispatch('toggleBlobView')
.then(() => {
expect(store.state.currentBlobView).toBe('repo-editor');
done();
})
.catch(done.fail);
});
it('sets preview mode view if not in edit mode', (done) => {
store.dispatch('toggleBlobView')
.then(() => {
expect(store.state.currentBlobView).toBe('repo-preview');
done();
})
.catch(done.fail);
});
});
describe('checkCommitStatus', () => {
beforeEach(() => {
store.state.project.id = 2;
store.state.currentBranch = 'master';
store.state.currentRef = '1';
});
it('calls service', (done) => {
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
commit: { id: '123' },
}));
store.dispatch('checkCommitStatus')
.then(() => {
expect(service.getBranchData).toHaveBeenCalledWith(2, 'master');
done();
})
.catch(done.fail);
});
it('returns true if current ref does not equal returned ID', (done) => {
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
commit: { id: '123' },
}));
store.dispatch('checkCommitStatus')
.then((val) => {
expect(val).toBeTruthy();
done();
})
.catch(done.fail);
});
it('returns false if current ref equals returned ID', (done) => {
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
commit: { id: '1' },
}));
store.dispatch('checkCommitStatus')
.then((val) => {
expect(val).toBeFalsy();
done();
})
.catch(done.fail);
});
});
describe('commitChanges', () => {
let payload;
beforeEach(() => {
spyOn(window, 'scrollTo');
document.body.innerHTML += '<div class="flash-container"></div>';
store.state.project.id = 123;
payload = {
branch: 'master',
};
});
afterEach(() => {
document.querySelector('.flash-container').remove();
});
describe('success', () => {
beforeEach(() => {
spyOn(service, 'commit').and.returnValue(Promise.resolve({
id: '123456',
short_id: '123',
message: 'test message',
committed_date: 'date',
stats: {
additions: '1',
deletions: '2',
},
}));
});
it('calls service', (done) => {
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
expect(service.commit).toHaveBeenCalledWith(123, payload);
done();
}).catch(done.fail);
});
it('shows flash notice', (done) => {
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
const alert = document.querySelector('.flash-container');
expect(alert.querySelector('.flash-notice')).not.toBeNull();
expect(alert.textContent.trim()).toBe(
'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.',
);
done();
}).catch(done.fail);
});
it('adds commit data to changed files', (done) => {
const changedFile = file();
const f = file();
changedFile.changed = true;
store.state.openFiles.push(changedFile, f);
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
expect(changedFile.lastCommit.message).toBe('test message');
expect(f.lastCommit.message).not.toBe('test message');
done();
}).catch(done.fail);
});
it('toggles edit mode', (done) => {
store.state.editMode = true;
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
expect(store.state.editMode).toBeFalsy();
done();
}).catch(done.fail);
});
it('closes all files', (done) => {
store.state.openFiles.push(file());
store.state.openFiles[0].opened = true;
store.dispatch('commitChanges', { payload, newMr: false })
.then(Vue.nextTick)
.then(() => {
expect(store.state.openFiles.length).toBe(0);
done();
}).catch(done.fail);
});
it('scrolls to top of page', (done) => {
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
done();
}).catch(done.fail);
});
it('updates commit ref', (done) => {
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
expect(store.state.currentRef).toBe('123456');
done();
}).catch(done.fail);
});
it('redirects to new merge request page', (done) => {
spyOn(gl.utils, 'visitUrl');
store.state.endpoints.newMergeRequestUrl = 'newMergeRequestUrl?branch=';
store.dispatch('commitChanges', { payload, newMr: true })
.then(() => {
expect(gl.utils.visitUrl).toHaveBeenCalledWith('newMergeRequestUrl?branch=master');
done();
}).catch(done.fail);
});
});
describe('failed', () => {
beforeEach(() => {
spyOn(service, 'commit').and.returnValue(Promise.resolve({
message: 'failed message',
}));
});
it('shows failed message', (done) => {
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
const alert = document.querySelector('.flash-container');
expect(alert.textContent.trim()).toBe(
'failed message',
);
done();
}).catch(done.fail);
});
});
});
describe('createTempEntry', () => {
it('creates a temp tree', (done) => {
store.dispatch('createTempEntry', {
name: 'test',
type: 'tree',
})
.then(() => {
expect(store.state.tree.length).toBe(1);
expect(store.state.tree[0].tempFile).toBeTruthy();
expect(store.state.tree[0].type).toBe('tree');
done();
})
.catch(done.fail);
});
it('creates temp file', (done) => {
store.dispatch('createTempEntry', {
name: 'test',
type: 'blob',
})
.then(() => {
expect(store.state.tree.length).toBe(1);
expect(store.state.tree[0].tempFile).toBeTruthy();
expect(store.state.tree[0].type).toBe('blob');
done();
})
.catch(done.fail);
});
});
describe('popHistoryState', () => {
});
describe('scrollToTab', () => {
it('focuses the current active element', (done) => {
document.body.innerHTML += '<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>';
const el = document.querySelector('.repo-tab');
spyOn(el, 'focus');
store.dispatch('scrollToTab')
.then(() => {
setTimeout(() => {
expect(el.focus).toHaveBeenCalled();
document.getElementById('tabs').remove();
done();
});
})
.catch(done.fail);
});
});
});
import * as getters from '~/repo/stores/getters';
import state from '~/repo/stores/state';
import { file } from '../helpers';
describe('Multi-file store getters', () => {
let localState;
beforeEach(() => {
localState = state();
});
describe('treeList', () => {
it('returns flat tree list', () => {
localState.tree.push(file('1'));
localState.tree[0].tree.push(file('2'));
localState.tree[0].tree[0].tree.push(file('3'));
const treeList = getters.treeList(localState);
expect(treeList.length).toBe(3);
expect(treeList[1].name).toBe(localState.tree[0].tree[0].name);
expect(treeList[2].name).toBe(localState.tree[0].tree[0].tree[0].name);
});
});
describe('changedFiles', () => {
it('returns a list of changed opened files', () => {
localState.openFiles.push(file());
localState.openFiles.push(file('changed'));
localState.openFiles[1].changed = true;
const changedFiles = getters.changedFiles(localState);
expect(changedFiles.length).toBe(1);
expect(changedFiles[0].name).toBe('changed');
});
});
describe('activeFile', () => {
it('returns the current active file', () => {
localState.openFiles.push(file());
localState.openFiles.push(file('active'));
localState.openFiles[1].active = true;
expect(getters.activeFile(localState).name).toBe('active');
});
it('returns undefined if no active files are found', () => {
localState.openFiles.push(file());
localState.openFiles.push(file('active'));
expect(getters.activeFile(localState)).toBeUndefined();
});
});
describe('activeFileExtension', () => {
it('returns the file extension for the current active file', () => {
localState.openFiles.push(file('active'));
localState.openFiles[0].active = true;
localState.openFiles[0].path = 'test.js';
expect(getters.activeFileExtension(localState)).toBe('.js');
localState.openFiles[0].path = 'test.es6.js';
expect(getters.activeFileExtension(localState)).toBe('.js');
});
});
describe('isCollapsed', () => {
it('returns true if state has open files', () => {
localState.openFiles.push(file());
expect(getters.isCollapsed(localState)).toBeTruthy();
});
it('returns false if state has no open files', () => {
expect(getters.isCollapsed(localState)).toBeFalsy();
});
});
describe('canEditFile', () => {
beforeEach(() => {
localState.onTopOfBranch = true;
localState.canCommit = true;
localState.openFiles.push(file());
localState.openFiles[0].active = true;
});
it('returns true if user can commit and has open files', () => {
expect(getters.canEditFile(localState)).toBeTruthy();
});
it('returns false if user can commit and has no open files', () => {
localState.openFiles = [];
expect(getters.canEditFile(localState)).toBeFalsy();
});
it('returns false if user can commit and active file is binary', () => {
localState.openFiles[0].binary = true;
expect(getters.canEditFile(localState)).toBeFalsy();
});
it('returns false if user cant commit', () => {
localState.canCommit = false;
expect(getters.canEditFile(localState)).toBeFalsy();
});
it('returns false if user can commit but on a branch', () => {
localState.onTopOfBranch = false;
expect(getters.canEditFile(localState)).toBeFalsy();
});
});
});
import mutations from '~/repo/stores/mutations/branch';
import state from '~/repo/stores/state';
describe('Multi-file store branch mutations', () => {
let localState;
beforeEach(() => {
localState = state();
});
describe('SET_CURRENT_BRANCH', () => {
it('sets currentBranch', () => {
mutations.SET_CURRENT_BRANCH(localState, 'master');
expect(localState.currentBranch).toBe('master');
});
});
});
import mutations from '~/repo/stores/mutations/file';
import state from '~/repo/stores/state';
import { file } from '../../helpers';
describe('Multi-file store file mutations', () => {
let localState;
let localFile;
beforeEach(() => {
localState = state();
localFile = file();
});
describe('SET_FILE_ACTIVE', () => {
it('sets the file active', () => {
mutations.SET_FILE_ACTIVE(localState, {
file: localFile,
active: true,
});
expect(localFile.active).toBeTruthy();
});
});
describe('TOGGLE_FILE_OPEN', () => {
beforeEach(() => {
mutations.TOGGLE_FILE_OPEN(localState, localFile);
});
it('adds into opened files', () => {
expect(localFile.opened).toBeTruthy();
expect(localState.openFiles.length).toBe(1);
});
it('removes from opened files', () => {
mutations.TOGGLE_FILE_OPEN(localState, localFile);
expect(localFile.opened).toBeFalsy();
expect(localState.openFiles.length).toBe(0);
});
});
describe('SET_FILE_DATA', () => {
it('sets extra file data', () => {
mutations.SET_FILE_DATA(localState, {
data: {
blame_path: 'blame',
commits_path: 'commits',
permalink: 'permalink',
raw_path: 'raw',
binary: true,
html: 'html',
render_error: 'render_error',
},
file: localFile,
});
expect(localFile.blamePath).toBe('blame');
expect(localFile.commitsPath).toBe('commits');
expect(localFile.permalink).toBe('permalink');
expect(localFile.rawPath).toBe('raw');
expect(localFile.binary).toBeTruthy();
expect(localFile.html).toBe('html');
expect(localFile.renderError).toBe('render_error');
});
});
describe('SET_FILE_RAW_DATA', () => {
it('sets raw data', () => {
mutations.SET_FILE_RAW_DATA(localState, {
file: localFile,
raw: 'testing',
});
expect(localFile.raw).toBe('testing');
});
});
describe('UPDATE_FILE_CONTENT', () => {
beforeEach(() => {
localFile.raw = 'test';
});
it('sets content', () => {
mutations.UPDATE_FILE_CONTENT(localState, {
file: localFile,
content: 'test',
});
expect(localFile.content).toBe('test');
});
it('sets changed if content does not match raw', () => {
mutations.UPDATE_FILE_CONTENT(localState, {
file: localFile,
content: 'testing',
});
expect(localFile.content).toBe('testing');
expect(localFile.changed).toBeTruthy();
});
});
describe('DISCARD_FILE_CHANGES', () => {
beforeEach(() => {
localFile.content = 'test';
localFile.changed = true;
});
it('resets content and changed', () => {
mutations.DISCARD_FILE_CHANGES(localState, localFile);
expect(localFile.content).toBe('');
expect(localFile.changed).toBeFalsy();
});
});
describe('CREATE_TMP_FILE', () => {
it('adds file into parent tree', () => {
const f = file();
mutations.CREATE_TMP_FILE(localState, {
file: f,
parent: localFile,
});
expect(localFile.tree.length).toBe(1);
expect(localFile.tree[0].name).toBe(f.name);
});
});
});
import mutations from '~/repo/stores/mutations/tree';
import state from '~/repo/stores/state';
import { file } from '../../helpers';
describe('Multi-file store tree mutations', () => {
let localState;
let localTree;
beforeEach(() => {
localState = state();
localTree = file();
});
describe('TOGGLE_TREE_OPEN', () => {
it('toggles tree open', () => {
mutations.TOGGLE_TREE_OPEN(localState, localTree);
expect(localTree.opened).toBeTruthy();
mutations.TOGGLE_TREE_OPEN(localState, localTree);
expect(localTree.opened).toBeFalsy();
});
});
describe('SET_DIRECTORY_DATA', () => {
const data = [{
name: 'tree',
},
{
name: 'submodule',
},
{
name: 'blob',
}];
it('adds directory data', () => {
mutations.SET_DIRECTORY_DATA(localState, {
data,
tree: localState,
});
expect(localState.tree.length).toBe(3);
expect(localState.tree[0].name).toBe('tree');
expect(localState.tree[1].name).toBe('submodule');
expect(localState.tree[2].name).toBe('blob');
});
});
describe('SET_PARENT_TREE_URL', () => {
it('sets the parent tree url', () => {
mutations.SET_PARENT_TREE_URL(localState, 'test');
expect(localState.parentTreeUrl).toBe('test');
});
});
describe('CREATE_TMP_TREE', () => {
it('adds tree into parent tree', () => {
const tmpEntry = file();
mutations.CREATE_TMP_TREE(localState, {
tmpEntry,
parent: localTree,
});
expect(localTree.tree.length).toBe(1);
expect(localTree.tree[0].name).toBe(tmpEntry.name);
});
});
});
import mutations from '~/repo/stores/mutations';
import state from '~/repo/stores/state';
import { file } from '../helpers';
describe('Multi-file store mutations', () => {
let localState;
let entry;
beforeEach(() => {
localState = state();
entry = file();
});
describe('SET_INITIAL_DATA', () => {
it('sets all initial data', () => {
mutations.SET_INITIAL_DATA(localState, {
test: 'test',
});
expect(localState.test).toBe('test');
});
});
describe('SET_PREVIEW_MODE', () => {
it('sets currentBlobView to repo-preview', () => {
mutations.SET_PREVIEW_MODE(localState);
expect(localState.currentBlobView).toBe('repo-preview');
localState.currentBlobView = 'testing';
mutations.SET_PREVIEW_MODE(localState);
expect(localState.currentBlobView).toBe('repo-preview');
});
});
describe('SET_EDIT_MODE', () => {
it('sets currentBlobView to repo-editor', () => {
mutations.SET_EDIT_MODE(localState);
expect(localState.currentBlobView).toBe('repo-editor');
localState.currentBlobView = 'testing';
mutations.SET_EDIT_MODE(localState);
expect(localState.currentBlobView).toBe('repo-editor');
});
});
describe('TOGGLE_LOADING', () => {
it('toggles loading of entry', () => {
mutations.TOGGLE_LOADING(localState, entry);
expect(entry.loading).toBeTruthy();
mutations.TOGGLE_LOADING(localState, entry);
expect(entry.loading).toBeFalsy();
});
});
describe('TOGGLE_EDIT_MODE', () => {
it('toggles editMode', () => {
mutations.TOGGLE_EDIT_MODE(localState);
expect(localState.editMode).toBeTruthy();
mutations.TOGGLE_EDIT_MODE(localState);
expect(localState.editMode).toBeFalsy();
});
});
describe('TOGGLE_DISCARD_POPUP', () => {
it('sets discardPopupOpen', () => {
mutations.TOGGLE_DISCARD_POPUP(localState, true);
expect(localState.discardPopupOpen).toBeTruthy();
mutations.TOGGLE_DISCARD_POPUP(localState, false);
expect(localState.discardPopupOpen).toBeFalsy();
});
});
describe('SET_COMMIT_REF', () => {
it('sets currentRef', () => {
mutations.SET_COMMIT_REF(localState, '123');
expect(localState.currentRef).toBe('123');
});
});
describe('SET_ROOT', () => {
it('sets isRoot & initialRoot', () => {
mutations.SET_ROOT(localState, true);
expect(localState.isRoot).toBeTruthy();
expect(localState.isInitialRoot).toBeTruthy();
mutations.SET_ROOT(localState, false);
expect(localState.isRoot).toBeFalsy();
expect(localState.isInitialRoot).toBeFalsy();
});
});
describe('SET_PREVIOUS_URL', () => {
it('sets previousUrl', () => {
mutations.SET_PREVIOUS_URL(localState, 'testing');
expect(localState.previousUrl).toBe('testing');
});
});
});
import * as utils from '~/repo/stores/utils';
describe('Multi-file store utils', () => {
describe('setPageTitle', () => {
it('sets the document page title', () => {
utils.setPageTitle('test');
expect(document.title).toBe('test');
});
});
describe('pushState', () => {
it('calls history.pushState', () => {
spyOn(history, 'pushState');
utils.pushState('test');
expect(history.pushState).toHaveBeenCalledWith({ url: 'test' }, '', 'test');
});
});
describe('createTemp', () => {
it('creates temp tree', () => {
const tmp = utils.createTemp({
name: 'test',
path: 'test',
type: 'tree',
level: 0,
changed: false,
content: '',
base64: '',
});
expect(tmp.tempFile).toBeTruthy();
expect(tmp.icon).toBe('fa-folder');
});
it('creates temp file', () => {
const tmp = utils.createTemp({
name: 'test',
path: 'test',
type: 'blob',
level: 0,
changed: false,
content: '',
base64: '',
});
expect(tmp.tempFile).toBeTruthy();
expect(tmp.icon).toBe('fa-file-text-o');
});
});
describe('findIndexOfFile', () => {
let state;
beforeEach(() => {
state = [{
path: '1',
}, {
path: '2',
}];
});
it('finds in the index of an entry by path', () => {
const index = utils.findIndexOfFile(state, {
path: '2',
});
expect(index).toBe(1);
});
});
describe('findEntry', () => {
let state;
beforeEach(() => {
state = {
tree: [{
type: 'tree',
name: 'test',
}, {
type: 'blob',
name: 'file',
}],
};
});
it('returns an entry found by name', () => {
const foundEntry = utils.findEntry(state, 'tree', 'test');
expect(foundEntry.type).toBe('tree');
expect(foundEntry.name).toBe('test');
});
it('returns undefined when no entry found', () => {
const foundEntry = utils.findEntry(state, 'blob', 'test');
expect(foundEntry).toBeUndefined();
});
});
});
require 'spec_helper'
describe Gitlab::Utils::StrongMemoize do
let(:klass) do
struct = Struct.new(:value) do
def method_name
strong_memoize(:method_name) do
trace << value
value
end
end
def trace
@trace ||= []
end
end
struct.include(described_class)
struct
end
subject(:object) { klass.new(value) }
shared_examples 'caching the value' do
it 'only calls the block once' do
value0 = object.method_name
value1 = object.method_name
expect(value0).to eq(value)
expect(value1).to eq(value)
expect(object.trace).to contain_exactly(value)
end
it 'returns and defines the instance variable for the exact value' do
returned_value = object.method_name
memoized_value = object.instance_variable_get(:@method_name)
expect(returned_value).to eql(value)
expect(memoized_value).to eql(value)
end
end
describe '#strong_memoize' do
[nil, false, true, 'value', 0, [0]].each do |value|
context "with value #{value}" do
let(:value) { value }
it_behaves_like 'caching the value'
end
end
end
end
......@@ -783,7 +783,25 @@ describe Notify do
shared_examples 'an email for a note on a diff discussion' do |model|
let(:note) { create(model, author: note_author) }
it "includes diffs with character-level highlighting" do
context 'when note is on image' do
before do
allow_any_instance_of(DiffDiscussion).to receive(:on_image?).and_return(true)
end
it 'does not include diffs with character-level highlighting' do
is_expected.not_to have_body_text '<span class="p">}</span></span>'
end
it 'ends the intro with a dot' do
is_expected.to have_body_text "#{note.diff_file.file_path}</a>."
end
end
it 'ends the intro with a colon' do
is_expected.to have_body_text "#{note.diff_file.file_path}</a>:"
end
it 'includes diffs with character-level highlighting' do
is_expected.to have_body_text '<span class="p">}</span></span>'
end
......
......@@ -186,4 +186,21 @@ describe Milestone, 'Milestoneish' do
expect(milestone.elapsed_days).to eq(2)
end
end
describe '#total_issue_time_spent' do
it 'calculates total issue time spent' do
closed_issue_1.spend_time(duration: 300, user: author)
closed_issue_1.save!
closed_issue_2.spend_time(duration: 600, user: assignee)
closed_issue_2.save!
expect(milestone.total_issue_time_spent).to eq(900)
end
end
describe '#human_total_issue_time_spent' do
it 'returns nil if no time has been spent' do
expect(milestone.human_total_issue_time_spent).to be_nil
end
end
end
......@@ -765,22 +765,4 @@ describe Issue do
expect(described_class.public_only).to eq([public_issue])
end
end
describe '#update_project_counter_caches?' do
it 'returns true when the state changes' do
subject.state = 'closed'
expect(subject.update_project_counter_caches?).to eq(true)
end
it 'returns true when the confidential flag changes' do
subject.confidential = true
expect(subject.update_project_counter_caches?).to eq(true)
end
it 'returns false when the state or confidential flag did not change' do
expect(subject.update_project_counter_caches?).to eq(false)
end
end
end
......@@ -1772,16 +1772,4 @@ describe MergeRequest do
.to change { project.open_merge_requests_count }.from(1).to(0)
end
end
describe '#update_project_counter_caches?' do
it 'returns true when the state changes' do
subject.state = 'closed'
expect(subject.update_project_counter_caches?).to eq(true)
end
it 'returns false when the state did not change' do
expect(subject.update_project_counter_caches?).to eq(false)
end
end
end
......@@ -3,6 +3,7 @@ require 'rails_helper'
describe API::PagesDomains do
set(:project) { create(:project) }
set(:user) { create(:user) }
set(:admin) { create(:admin) }
set(:pages_domain) { create(:pages_domain, domain: 'www.domain.test', project: project) }
set(:pages_domain_secure) { create(:pages_domain, :with_certificate, :with_key, domain: 'ssl.domain.test', project: project) }
......@@ -23,12 +24,49 @@ describe API::PagesDomains do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
end
describe 'GET /pages/domains' do
context 'when pages is disabled' do
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
end
it_behaves_like '404 response' do
let(:request) { get api('/pages/domains', admin) }
end
end
context 'when pages is enabled' do
context 'when authenticated as an admin' do
it 'returns paginated all pages domains' do
get api('/pages/domains', admin)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain_basics')
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
expect(json_response.last).to have_key('domain')
expect(json_response.last).to have_key('certificate_expiration')
expect(json_response.last['certificate_expiration']['expired']).to be true
expect(json_response.first).not_to have_key('certificate_expiration')
end
end
context 'when authenticated as a non-member' do
it_behaves_like '403 response' do
let(:request) { get api('/pages/domains', user) }
end
end
end
end
describe 'GET /projects/:project_id/pages/domains' do
shared_examples_for 'get pages domains' do
it 'returns paginated pages domains' do
get api(route, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domains')
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
......@@ -99,6 +137,7 @@ describe API::PagesDomains do
get api(route_domain, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(json_response['domain']).to eq(pages_domain.domain)
expect(json_response['url']).to eq(pages_domain.url)
expect(json_response['certificate']).to be_nil
......@@ -108,6 +147,7 @@ describe API::PagesDomains do
get api(route_secure_domain, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(json_response['domain']).to eq(pages_domain_secure.domain)
expect(json_response['url']).to eq(pages_domain_secure.url)
expect(json_response['certificate']['subject']).to eq(pages_domain_secure.subject)
......@@ -118,6 +158,7 @@ describe API::PagesDomains do
get api(route_expired_domain, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(json_response['certificate']['expired']).to be true
end
end
......@@ -187,6 +228,7 @@ describe API::PagesDomains do
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
expect(response).to have_gitlab_http_status(201)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain.domain).to eq(params[:domain])
expect(pages_domain.certificate).to be_nil
expect(pages_domain.key).to be_nil
......@@ -197,6 +239,7 @@ describe API::PagesDomains do
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
expect(response).to have_gitlab_http_status(201)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain.domain).to eq(params_secure[:domain])
expect(pages_domain.certificate).to eq(params_secure[:certificate])
expect(pages_domain.key).to eq(params_secure[:key])
......@@ -270,6 +313,7 @@ describe API::PagesDomains do
pages_domain_secure.reload
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain_secure.certificate).to be_nil
expect(pages_domain_secure.key).to be_nil
end
......@@ -279,6 +323,7 @@ describe API::PagesDomains do
pages_domain.reload
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain.certificate).to eq(params_secure[:certificate])
expect(pages_domain.key).to eq(params_secure[:key])
end
......@@ -288,6 +333,7 @@ describe API::PagesDomains do
pages_domain_expired.reload
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain_expired.certificate).to eq(params_secure[:certificate])
expect(pages_domain_expired.key).to eq(params_secure[:key])
end
......@@ -297,6 +343,7 @@ describe API::PagesDomains do
pages_domain_secure.reload
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain_secure.certificate).to eq(params_secure_nokey[:certificate])
end
......
// Converting text to basic latin (aka removing accents)
//
// Based on: http://semplicewebsites.com/removing-accents-javascript
//
var Latinise = {
map: {"Á":"A","Ă":"A","":"A","":"A","":"A","":"A","":"A","Ǎ":"A","Â":"A","":"A","":"A","":"A","":"A","":"A","Ä":"A","Ǟ":"A","Ȧ":"A","Ǡ":"A","":"A","Ȁ":"A","À":"A","":"A","Ȃ":"A","Ā":"A","Ą":"A","Å":"A","Ǻ":"A","":"A","Ⱥ":"A","Ã":"A","":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","":"AO","":"AU","":"AV","":"AV","":"AY","":"B","":"B","Ɓ":"B","":"B","Ƀ":"B","Ƃ":"B","Ć":"C","Č":"C","Ç":"C","":"C","Ĉ":"C","Ċ":"C","Ƈ":"C","Ȼ":"C","Ď":"D","":"D","":"D","":"D","":"D","Ɗ":"D","":"D","Dz":"D","Dž":"D","Đ":"D","Ƌ":"D","DZ":"DZ","DŽ":"DZ","É":"E","Ĕ":"E","Ě":"E","Ȩ":"E","":"E","Ê":"E","":"E","":"E","":"E","":"E","":"E","":"E","Ë":"E","Ė":"E","":"E","Ȅ":"E","È":"E","":"E","Ȇ":"E","Ē":"E","":"E","":"E","Ę":"E","Ɇ":"E","":"E","":"E","":"ET","":"F","Ƒ":"F","Ǵ":"G","Ğ":"G","Ǧ":"G","Ģ":"G","Ĝ":"G","Ġ":"G","Ɠ":"G","":"G","Ǥ":"G","":"H","Ȟ":"H","":"H","Ĥ":"H","":"H","":"H","":"H","":"H","Ħ":"H","Í":"I","Ĭ":"I","Ǐ":"I","Î":"I","Ï":"I","":"I","İ":"I","":"I","Ȉ":"I","Ì":"I","":"I","Ȋ":"I","Ī":"I","Į":"I","Ɨ":"I","Ĩ":"I","":"I","":"D","":"F","":"G","":"R","":"S","":"T","":"IS","Ĵ":"J","Ɉ":"J","":"K","Ǩ":"K","Ķ":"K","":"K","":"K","":"K","Ƙ":"K","":"K","":"K","":"K","Ĺ":"L","Ƚ":"L","Ľ":"L","Ļ":"L","":"L","":"L","":"L","":"L","":"L","":"L","Ŀ":"L","":"L","Lj":"L","Ł":"L","LJ":"LJ","":"M","":"M","":"M","":"M","Ń":"N","Ň":"N","Ņ":"N","":"N","":"N","":"N","Ǹ":"N","Ɲ":"N","":"N","Ƞ":"N","Nj":"N","Ñ":"N","NJ":"NJ","Ó":"O","Ŏ":"O","Ǒ":"O","Ô":"O","":"O","":"O","":"O","":"O","":"O","Ö":"O","Ȫ":"O","Ȯ":"O","Ȱ":"O","":"O","Ő":"O","Ȍ":"O","Ò":"O","":"O","Ơ":"O","":"O","":"O","":"O","":"O","":"O","Ȏ":"O","":"O","":"O","Ō":"O","":"O","":"O","Ɵ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Õ":"O","":"O","":"O","Ȭ":"O","Ƣ":"OI","":"OO","Ɛ":"E","Ɔ":"O","Ȣ":"OU","":"P","":"P","":"P","Ƥ":"P","":"P","":"P","":"P","":"Q","":"Q","Ŕ":"R","Ř":"R","Ŗ":"R","":"R","":"R","":"R","Ȑ":"R","Ȓ":"R","":"R","Ɍ":"R","":"R","":"C","Ǝ":"E","Ś":"S","":"S","Š":"S","":"S","Ş":"S","Ŝ":"S","Ș":"S","":"S","":"S","":"S","":"SS","Ť":"T","Ţ":"T","":"T","Ț":"T","Ⱦ":"T","":"T","":"T","Ƭ":"T","":"T","Ʈ":"T","Ŧ":"T","":"A","":"L","Ɯ":"M","Ʌ":"V","":"TZ","Ú":"U","Ŭ":"U","Ǔ":"U","Û":"U","":"U","Ü":"U","Ǘ":"U","Ǚ":"U","Ǜ":"U","Ǖ":"U","":"U","":"U","Ű":"U","Ȕ":"U","Ù":"U","":"U","Ư":"U","":"U","":"U","":"U","":"U","":"U","Ȗ":"U","Ū":"U","":"U","Ų":"U","Ů":"U","Ũ":"U","":"U","":"U","":"V","":"V","Ʋ":"V","":"V","":"VY","":"W","Ŵ":"W","":"W","":"W","":"W","":"W","":"W","":"X","":"X","Ý":"Y","Ŷ":"Y","Ÿ":"Y","":"Y","":"Y","":"Y","Ƴ":"Y","":"Y","":"Y","Ȳ":"Y","Ɏ":"Y","":"Y","Ź":"Z","Ž":"Z","":"Z","":"Z","Ż":"Z","":"Z","Ȥ":"Z","":"Z","Ƶ":"Z","IJ":"IJ","Œ":"OE","":"A","":"AE","ʙ":"B","":"B","":"C","":"D","":"E","":"F","ɢ":"G","ʛ":"G","ʜ":"H","ɪ":"I","ʁ":"R","":"J","":"K","ʟ":"L","":"L","":"M","ɴ":"N","":"O","ɶ":"OE","":"O","":"OU","":"P","ʀ":"R","":"N","":"R","":"S","":"T","":"E","":"R","":"U","":"V","":"W","ʏ":"Y","":"Z","á":"a","ă":"a","":"a","":"a","":"a","":"a","":"a","ǎ":"a","â":"a","":"a","":"a","":"a","":"a","":"a","ä":"a","ǟ":"a","ȧ":"a","ǡ":"a","":"a","ȁ":"a","à":"a","":"a","ȃ":"a","ā":"a","ą":"a","":"a","":"a","å":"a","ǻ":"a","":"a","":"a","ã":"a","":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","":"ao","":"au","":"av","":"av","":"ay","":"b","":"b","ɓ":"b","":"b","":"b","":"b","ƀ":"b","ƃ":"b","ɵ":"o","ć":"c","č":"c","ç":"c","":"c","ĉ":"c","ɕ":"c","ċ":"c","ƈ":"c","ȼ":"c","ď":"d","":"d","":"d","ȡ":"d","":"d","":"d","ɗ":"d","":"d","":"d","":"d","":"d","đ":"d","ɖ":"d","ƌ":"d","ı":"i","ȷ":"j","ɟ":"j","ʄ":"j","dz":"dz","dž":"dz","é":"e","ĕ":"e","ě":"e","ȩ":"e","":"e","ê":"e","ế":"e","":"e","":"e","":"e","":"e","":"e","ë":"e","ė":"e","":"e","ȅ":"e","è":"e","":"e","ȇ":"e","ē":"e","":"e","":"e","":"e","ę":"e","":"e","ɇ":"e","":"e","":"e","":"et","":"f","ƒ":"f","":"f","":"f","ǵ":"g","ğ":"g","ǧ":"g","ģ":"g","ĝ":"g","ġ":"g","ɠ":"g","":"g","":"g","ǥ":"g","":"h","ȟ":"h","":"h","ĥ":"h","":"h","":"h","":"h","":"h","ɦ":"h","":"h","ħ":"h","ƕ":"hv","í":"i","ĭ":"i","ǐ":"i","î":"i","ï":"i","":"i","":"i","ȉ":"i","ì":"i","":"i","ȋ":"i","ī":"i","į":"i","":"i","ɨ":"i","ĩ":"i","":"i","":"d","":"f","":"g","":"r","":"s","":"t","":"is","ǰ":"j","ĵ":"j","ʝ":"j","ɉ":"j","":"k","ǩ":"k","ķ":"k","":"k","":"k","":"k","ƙ":"k","":"k","":"k","":"k","":"k","ĺ":"l","ƚ":"l","ɬ":"l","ľ":"l","ļ":"l","":"l","ȴ":"l","":"l","":"l","":"l","":"l","":"l","ŀ":"l","ɫ":"l","":"l","ɭ":"l","ł":"l","lj":"lj","ſ":"s","":"s","":"s","":"s","ḿ":"m","":"m","":"m","ɱ":"m","":"m","":"m","ń":"n","ň":"n","ņ":"n","":"n","ȵ":"n","":"n","":"n","ǹ":"n","ɲ":"n","":"n","ƞ":"n","":"n","":"n","ɳ":"n","ñ":"n","nj":"nj","ó":"o","ŏ":"o","ǒ":"o","ô":"o","":"o","":"o","":"o","":"o","":"o","ö":"o","ȫ":"o","ȯ":"o","ȱ":"o","":"o","ő":"o","ȍ":"o","ò":"o","":"o","ơ":"o","":"o","":"o","":"o","":"o","":"o","ȏ":"o","":"o","":"o","":"o","ō":"o","":"o","":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","õ":"o","":"o","":"o","ȭ":"o","ƣ":"oi","":"oo","ɛ":"e","":"e","ɔ":"o","":"o","ȣ":"ou","":"p","":"p","":"p","ƥ":"p","":"p","":"p","":"p","":"p","":"p","":"q","ʠ":"q","ɋ":"q","":"q","ŕ":"r","ř":"r","ŗ":"r","":"r","":"r","":"r","ȑ":"r","ɾ":"r","":"r","ȓ":"r","":"r","ɼ":"r","":"r","":"r","ɍ":"r","ɽ":"r","":"c","":"c","ɘ":"e","ɿ":"r","ś":"s","":"s","š":"s","":"s","ş":"s","ŝ":"s","ș":"s","":"s","":"s","":"s","ʂ":"s","":"s","":"s","ȿ":"s","ɡ":"g","ß":"ss","":"o","":"o","":"u","ť":"t","ţ":"t","":"t","ț":"t","ȶ":"t","":"t","":"t","":"t","":"t","ƭ":"t","":"t","":"t","ƫ":"t","ʈ":"t","ŧ":"t","":"th","ɐ":"a","":"ae","ǝ":"e","":"g","ɥ":"h","ʮ":"h","ʯ":"h","":"i","ʞ":"k","":"l","ɯ":"m","ɰ":"m","":"oe","ɹ":"r","ɻ":"r","ɺ":"r","":"r","ʇ":"t","ʌ":"v","ʍ":"w","ʎ":"y","":"tz","ú":"u","ŭ":"u","ǔ":"u","û":"u","":"u","ü":"u","ǘ":"u","ǚ":"u","ǜ":"u","ǖ":"u","":"u","":"u","ű":"u","ȕ":"u","ù":"u","":"u","ư":"u","":"u","":"u","":"u","":"u","":"u","ȗ":"u","ū":"u","":"u","ų":"u","":"u","ů":"u","ũ":"u","":"u","":"u","":"ue","":"um","":"v","":"v","ṿ":"v","ʋ":"v","":"v","":"v","":"v","":"vy","":"w","ŵ":"w","":"w","":"w","":"w","":"w","":"w","":"w","":"x","":"x","":"x","ý":"y","ŷ":"y","ÿ":"y","":"y","":"y","":"y","ƴ":"y","":"y","ỿ":"y","ȳ":"y","":"y","ɏ":"y","":"y","ź":"z","ž":"z","":"z","ʑ":"z","":"z","ż":"z","":"z","ȥ":"z","":"z","":"z","":"z","ʐ":"z","ƶ":"z","ɀ":"z","":"ff","":"ffi","":"ffl","":"fi","":"fl","ij":"ij","œ":"oe","":"st","":"a","":"e","":"i","":"j","":"o","":"r","":"u","":"v","":"x"}
};
String.prototype.latinise = function() {
return this.replace(/[^A-Za-z0-9]/g, function(x) { return Latinise.map[x] || x; });
};
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment