diff options
39 files changed, 2293 insertions, 315 deletions
diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js index 3de192d56eb..d2d3a257c0d 100644 --- a/app/assets/javascripts/abuse_reports.js +++ b/app/assets/javascripts/abuse_reports.js @@ -1,3 +1,5 @@ +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)); } } diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index de9e44cef35..182957113a2 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -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: { diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index ae6b8902032..9b952ea7b60 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -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')); diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js index 3bed0678350..9a4c9bfcc80 100644 --- a/app/assets/javascripts/create_label.js +++ b/app/assets/javascripts/create_label.js @@ -1,5 +1,6 @@ /* 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/>'); } diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js index 8bf9ae17de0..a8cd8c20f8f 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js @@ -1,7 +1,7 @@ /* 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]; diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index fc0308b81ba..9d25f806c0d 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -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, }; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index c4202f92443..4e7a6e54f90 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -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) { diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 48cd43d3348..d0f9e6af0f8 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -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() { diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index acd5730cf3c..7de07e9403d 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,6 +1,6 @@ /* 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) { diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 07899777a1e..5c4926d6ac8 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -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; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 29fc91733b3..5679b8c9a09 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -2,6 +2,7 @@ import timeago from 'timeago.js'; import dateFormat from 'vendor/date.format'; +import { pluralize } from './text_utility'; import { lang, @@ -143,9 +144,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; } diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js new file mode 100644 index 00000000000..2dc9cf0cc29 --- /dev/null +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -0,0 +1,153 @@ +/* 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; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index f776829f69c..28ab9dddc4c 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,18 +1,13 @@ -/* 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,178 +15,43 @@ gl.text.addDelimiter = function(text) { * @param {Number} count * @return {Number|String} */ -export function highCountTrim(count) { - return count > 99 ? '99+' : count; -} - -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' : ''; +export const highCountTrim = count => (count > 99 ? '99+' : count); - 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(); -}; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 31c5cfc5e55..127fddcf8d3 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -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 diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index af0658eb668..d30ff12bb59 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -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() { diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 547140b1a43..19d8e1f49cf 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,7 +1,7 @@ <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}`; }, }, diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js index be290c268b1..120ce96f44d 100644 --- a/app/assets/javascripts/repo/stores/actions.js +++ b/app/assets/javascripts/repo/stores/actions.js @@ -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); diff --git a/app/assets/javascripts/repo/stores/actions/branch.js b/app/assets/javascripts/repo/stores/actions/branch.js index b81a70dfd1e..61d9a5af3e3 100644 --- a/app/assets/javascripts/repo/stores/actions/branch.js +++ b/app/assets/javascripts/repo/stores/actions/branch.js @@ -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); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js index 219ff94924e..13e4cb5717e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js @@ -1,5 +1,5 @@ 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 diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js index a0025ddb598..7a865587444 100644 --- a/app/assets/javascripts/wikis.js +++ b/app/assets/javascripts/wikis.js @@ -1,4 +1,5 @@ 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'); diff --git a/changelogs/unreleased/text-utils.yml b/changelogs/unreleased/text-utils.yml new file mode 100644 index 00000000000..b95bb82fe01 --- /dev/null +++ b/changelogs/unreleased/text-utils.yml @@ -0,0 +1,5 @@ +--- +title: Export text utils functions as es6 module and add tests +merge_request: +author: +type: other diff --git a/doc/api/services.md b/doc/api/services.md index 08a2bee1518..08df26db3ec 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -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. diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md index b8dd96087f1..43713855e26 100644 --- a/doc/user/project/members/index.md +++ b/doc/user/project/members/index.md @@ -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) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 14a8d551524..cfb88a0c12b 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -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 diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 5402d61da54..db5ce2d11a8 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -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) diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js new file mode 100644 index 00000000000..a95a7e2a5be --- /dev/null +++ b/spec/javascripts/lib/utils/text_markdown_spec.js @@ -0,0 +1,62 @@ +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}* `); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 829b3ef5735..b21bd958f90 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -1,109 +1,57 @@ -import { highCountTrim } 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(highCountTrim(105)).toBe('99+'); - expect(highCountTrim(100)).toBe('99+'); + expect(textUtils.highCountTrim(105)).toBe('99+'); + expect(textUtils.highCountTrim(100)).toBe('99+'); }); it('returns exact number for count < 100', () => { - expect(highCountTrim(45)).toBe(45); + expect(textUtils.highCountTrim(45)).toBe(45); }); }); - 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'); }); }); }); diff --git a/spec/javascripts/repo/helpers.js b/spec/javascripts/repo/helpers.js index 376c291c64b..820a44992b4 100644 --- a/spec/javascripts/repo/helpers.js +++ b/spec/javascripts/repo/helpers.js @@ -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(), - }, }); diff --git a/spec/javascripts/repo/stores/actions/branch_spec.js b/spec/javascripts/repo/stores/actions/branch_spec.js new file mode 100644 index 00000000000..af9d6835a67 --- /dev/null +++ b/spec/javascripts/repo/stores/actions/branch_spec.js @@ -0,0 +1,38 @@ +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); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js new file mode 100644 index 00000000000..099c0556e71 --- /dev/null +++ b/spec/javascripts/repo/stores/actions/file_spec.js @@ -0,0 +1,417 @@ +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); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/actions/tree_spec.js b/spec/javascripts/repo/stores/actions/tree_spec.js new file mode 100644 index 00000000000..393a797c6a3 --- /dev/null +++ b/spec/javascripts/repo/stores/actions/tree_spec.js @@ -0,0 +1,469 @@ +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); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js new file mode 100644 index 00000000000..f2a7a698912 --- /dev/null +++ b/spec/javascripts/repo/stores/actions_spec.js @@ -0,0 +1,419 @@ +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); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/getters_spec.js b/spec/javascripts/repo/stores/getters_spec.js new file mode 100644 index 00000000000..a204b2386cd --- /dev/null +++ b/spec/javascripts/repo/stores/getters_spec.js @@ -0,0 +1,119 @@ +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(); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/mutations/branch_spec.js b/spec/javascripts/repo/stores/mutations/branch_spec.js new file mode 100644 index 00000000000..3c06794d5e3 --- /dev/null +++ b/spec/javascripts/repo/stores/mutations/branch_spec.js @@ -0,0 +1,18 @@ +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'); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/mutations/file_spec.js b/spec/javascripts/repo/stores/mutations/file_spec.js new file mode 100644 index 00000000000..2f2835dde1f --- /dev/null +++ b/spec/javascripts/repo/stores/mutations/file_spec.js @@ -0,0 +1,131 @@ +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); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/mutations/tree_spec.js b/spec/javascripts/repo/stores/mutations/tree_spec.js new file mode 100644 index 00000000000..1c76cfed9c8 --- /dev/null +++ b/spec/javascripts/repo/stores/mutations/tree_spec.js @@ -0,0 +1,71 @@ +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); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/mutations_spec.js b/spec/javascripts/repo/stores/mutations_spec.js new file mode 100644 index 00000000000..d1c9885e01d --- /dev/null +++ b/spec/javascripts/repo/stores/mutations_spec.js @@ -0,0 +1,117 @@ +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'); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/utils_spec.js b/spec/javascripts/repo/stores/utils_spec.js new file mode 100644 index 00000000000..37287c587d7 --- /dev/null +++ b/spec/javascripts/repo/stores/utils_spec.js @@ -0,0 +1,102 @@ +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(); + }); + }); +}); diff --git a/vendor/assets/javascripts/latinise.js b/vendor/assets/javascripts/latinise.js deleted file mode 100644 index da37966b28a..00000000000 --- a/vendor/assets/javascripts/latinise.js +++ /dev/null @@ -1,11 +0,0 @@ -// 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":"ff","ffi":"ffi","ffl":"ffl","fi":"fi","fl":"fl","ij":"ij","œ":"oe","st":"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; }); -}; |