diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 18:44:42 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 18:44:42 +0300 |
commit | 4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch) | |
tree | 5423a1c7516cffe36384133ade12572cf709398d /app/assets/javascripts/lib/utils | |
parent | e570267f2f6b326480d284e0164a6464ba4081bc (diff) |
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'app/assets/javascripts/lib/utils')
-rw-r--r-- | app/assets/javascripts/lib/utils/common_utils.js | 3 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/datetime_utility.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/keys.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/number_utils.js | 10 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/recurrence.js | 154 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/text_markdown.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/uuids.js | 76 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/vuex_module_mappers.js | 91 |
8 files changed, 335 insertions, 4 deletions
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index fb257228597..8666d325c1b 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -645,9 +645,6 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => export const convertObjectPropsToSnakeCase = (obj = {}, options = {}) => convertObjectProps(convertToSnakeCase, obj, options); -export const imagePath = (imgUrl) => - `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`; - export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => { // Click a .js-select-on-focus field, select the contents // Prevent a mouseup event from deselecting the input diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index a509828815a..0a038febb9f 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -4,6 +4,8 @@ import { isString, mapValues, isNumber, reduce } from 'lodash'; import * as timeago from 'timeago.js'; import { languageCode, s__, __, n__ } from '../../locale'; +export const SECONDS_IN_DAY = 86400; + const DAYS_IN_WEEK = 7; window.timeago = timeago; diff --git a/app/assets/javascripts/lib/utils/keys.js b/app/assets/javascripts/lib/utils/keys.js index 2a8b1759e54..bd47f10b3ac 100644 --- a/app/assets/javascripts/lib/utils/keys.js +++ b/app/assets/javascripts/lib/utils/keys.js @@ -1,2 +1,3 @@ export const ESC_KEY = 'Escape'; export const ENTER_KEY = 'Enter'; +export const BACKSPACE_KEY = 'Backspace'; diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index 63feb6f9b1d..e3500d02a79 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -171,3 +171,13 @@ export const formattedChangeInPercent = (firstY, lastY, { nonFiniteResult = '-' export const isNumeric = (value) => { return !Number.isNaN(parseInt(value, 10)); }; + +const numberRegex = /^[0-9]+$/; + +/** + * Checks whether the value is a positive number or 0, or a string with equivalent value + * + * @param value + * @return {boolean} + */ +export const isPositiveInteger = (value) => numberRegex.test(value); diff --git a/app/assets/javascripts/lib/utils/recurrence.js b/app/assets/javascripts/lib/utils/recurrence.js new file mode 100644 index 00000000000..8fd26f3e393 --- /dev/null +++ b/app/assets/javascripts/lib/utils/recurrence.js @@ -0,0 +1,154 @@ +import { uuids } from './uuids'; + +/** + * @module recurrence + */ + +const instances = {}; + +/** + * Create a new unique {@link module:recurrence~RecurInstance|RecurInstance} + * @returns {module:recurrence.RecurInstance} The newly created {@link module:recurrence~RecurInstance|RecurInstance} + */ +export function create() { + const id = uuids()[0]; + let handlers = {}; + let count = 0; + + /** + * @namespace RecurInstance + * @description A RecurInstance tracks the count of any occurrence as registered by calls to <code>occur</code>. + * <br /><br /> + * It maintains an internal counter and a registry of handlers that can be arbitrarily assigned by a user. + * <br /><br /> + * While a RecurInstance isn't specific to any particular use-case, it may be useful for: + * <br /> + * <ul> + * <li>Tracking repeated errors across multiple - but not linked - network requests</li> + * <li>Tracking repeated user interactions (e.g. multiple clicks)</li> + * </ul> + * @summary A closure to track repeated occurrences of any arbitrary event. + * */ + const instance = { + /** + * @type {module:uuids~UUIDv4} + * @description A randomly generated {@link module:uuids~UUIDv4|UUID} for this particular recurrence instance + * @memberof module:recurrence~RecurInstance + * @readonly + * @inner + */ + get id() { + return id; + }, + /** + * @type {Number} + * @description The number of times this particular instance of recurrence has been triggered + * @memberof module:recurrence~RecurInstance + * @readonly + * @inner + */ + get count() { + return count; + }, + /** + * @type {Object} + * @description The handlers assigned to this recurrence tracker + * @example + * myRecurrence.handle( 4, () => console.log( "four" ) ); + * console.log( myRecurrence.handlers ); // {"4": () => console.log( "four" )} + * @memberof module:recurrence~RecurInstance + * @readonly + * @inner + */ + get handlers() { + return handlers; + }, + /** + * @type {Boolean} + * @description Delete any internal reference to the instance. + * <br /> + * Keep in mind that this will only attempt to remove the <strong>internal</strong> reference. + * <br /> + * If your code maintains a reference to the instance, the regular garbage collector will not free the memory. + * @memberof module:recurrence~RecurInstance + * @inner + */ + free() { + return delete instances[id]; + }, + /** + * @description Register a handler to be called when this occurrence is seen <code>onCount</code> number of times. + * @param {Number} onCount - The number of times the occurrence has been seen to respond to + * @param {Function} behavior - A callback function to run when the occurrence has been seen <code>onCount</code> times + * @memberof module:recurrence~RecurInstance + * @inner + */ + handle(onCount, behavior) { + if (onCount && behavior) { + handlers[onCount] = behavior; + } + }, + /** + * @description Remove the behavior callback handler that would be run when the occurrence is seen <code>onCount</code> times + * @param {Number} onCount - The count identifier for which to eject the callback handler + * @memberof module:recurrence~RecurInstance + * @inner + */ + eject(onCount) { + if (onCount) { + delete handlers[onCount]; + } + }, + /** + * @description Register that this occurrence has been seen and trigger any appropriate handlers + * @memberof module:recurrence~RecurInstance + * @inner + */ + occur() { + count += 1; + + if (typeof handlers[count] === 'function') { + handlers[count](count); + } + }, + /** + * @description Reset this recurrence instance without destroying it entirely + * @param {Object} [options] + * @param {Boolean} [options.currentCount = true] - Whether to reset the count + * @param {Boolean} [options.handlersList = false] - Whether to reset the list of attached handlers back to an empty state + * @memberof module:recurrence~RecurInstance + * @inner + */ + reset({ currentCount = true, handlersList = false } = {}) { + if (currentCount) { + count = 0; + } + + if (handlersList) { + handlers = {}; + } + }, + }; + + instances[id] = instance; + + return instance; +} + +/** + * Retrieve a stored {@link module:recurrence~RecurInstance|RecurInstance} by {@link module:uuids~UUIDv4|UUID} + * @param {module:uuids~UUIDv4} id - The {@link module:uuids~UUIDv4|UUID} of a previously created {@link module:recurrence~RecurInstance|RecurInstance} + * @returns {(module:recurrence~RecurInstance|undefined)} The {@link module:recurrence~RecurInstance|RecurInstance}, or undefined if the UUID doesn't refer to a known Instance + */ +export function recall(id) { + return instances[id]; +} + +/** + * Release the memory space for a given {@link module:recurrence~RecurInstance|RecurInstance} by {@link module:uuids~UUIDv4|UUID} + * @param {module:uuids~UUIDv4} id - The {@link module:uuids~UUIDv4|UUID} of a previously created {@link module:recurrence~RecurInstance|RecurInstance} + * @returns {Boolean} Whether the reference to the stored {@link module:recurrence~RecurInstance|RecurInstance} was released + */ +export function free(id) { + return recall(id)?.free() || false; +} diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 1593a363dd1..6ff2af47dd8 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -232,7 +232,7 @@ export function insertMarkdownText({ .join('\n'); } } else if (tag.indexOf(textPlaceholder) > -1) { - textToInsert = tag.replace(textPlaceholder, selected.replace(/\\n/g, '\n')); + textToInsert = tag.replace(textPlaceholder, () => selected.replace(/\\n/g, '\n')); } else { textToInsert = String(startChar) + tag + selected + (wrap ? tag : ''); } diff --git a/app/assets/javascripts/lib/utils/uuids.js b/app/assets/javascripts/lib/utils/uuids.js new file mode 100644 index 00000000000..98fe4bf9664 --- /dev/null +++ b/app/assets/javascripts/lib/utils/uuids.js @@ -0,0 +1,76 @@ +/** + * @module uuids + */ + +/** + * A string or number representing a start state for a random generator + * @typedef {(Number|String)} Seed + */ +/** + * A UUIDv4 string in the format <code>Hex{8}-Hex{4}-4Hex{3}-[89ab]Hex{3}-Hex{12}</code> + * @typedef {String} UUIDv4 + */ + +import { MersenneTwister } from 'fast-mersenne-twister'; +import { isString } from 'lodash'; +import stringHash from 'string-hash'; +import { v4 } from 'uuid'; + +function getSeed(seeds) { + return seeds.reduce((seedling, seed, i) => { + let thisSeed = 0; + + if (Number.isInteger(seed)) { + thisSeed = seed; + } else if (isString(seed)) { + thisSeed = stringHash(seed); + } + + return seedling + (seeds.length - i) * thisSeed; + }, 0); +} + +function getPseudoRandomNumberGenerator(...seeds) { + let seedNumber; + + if (seeds.length) { + seedNumber = getSeed(seeds); + } else { + seedNumber = Math.floor(Math.random() * 10 ** 15); + } + + return new MersenneTwister(seedNumber); +} + +function randomValuesForUuid(prng) { + const randomValues = []; + + for (let i = 0; i <= 3; i += 1) { + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + + view.setUint32(0, prng.randomNumber()); + + randomValues.push(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3)); + } + + return randomValues; +} + +/** + * Get an array of UUIDv4s + * @param {Object} [options={}] + * @param {Seed[]} [options.seeds=[]] - A list of mixed strings or numbers to seed the UUIDv4 generator + * @param {Number} [options.count=1] - A total number of UUIDv4s to generate + * @returns {UUIDv4[]} An array of UUIDv4s + */ +export function uuids({ seeds = [], count = 1 } = {}) { + const rng = getPseudoRandomNumberGenerator(...seeds); + return ( + // Create an array the same size as the number of UUIDs requested + Array(count) + .fill(0) + // Replace each slot in the array with a UUID which needs 16 (pseudo)random values to generate + .map(() => v4({ random: randomValuesForUuid(rng) })) + ); +} diff --git a/app/assets/javascripts/lib/utils/vuex_module_mappers.js b/app/assets/javascripts/lib/utils/vuex_module_mappers.js new file mode 100644 index 00000000000..95a794dd268 --- /dev/null +++ b/app/assets/javascripts/lib/utils/vuex_module_mappers.js @@ -0,0 +1,91 @@ +import { mapValues, isString } from 'lodash'; +import { mapState, mapActions } from 'vuex'; + +export const REQUIRE_STRING_ERROR_MESSAGE = + '`vuex_module_mappers` can only be used with an array of strings, or an object with string values. Consider using the regular `vuex` map helpers instead.'; + +const normalizeFieldsToObject = (fields) => { + return Array.isArray(fields) + ? fields.reduce((acc, key) => Object.assign(acc, { [key]: key }), {}) + : fields; +}; + +const mapVuexModuleFields = ({ namespaceSelector, fields, vuexHelper, selector } = {}) => { + // The `vuexHelper` needs an object which maps keys to field selector functions. + const map = mapValues(normalizeFieldsToObject(fields), (value) => { + if (!isString(value)) { + throw new Error(REQUIRE_STRING_ERROR_MESSAGE); + } + + // We need to use a good ol' function to capture the right "this". + return function mappedFieldSelector(...args) { + const namespace = namespaceSelector(this); + + return selector(namespace, value, ...args); + }; + }); + + return vuexHelper(map); +}; + +/** + * Like `mapState`, but takes a function in the first param for selecting a namespace. + * + * ``` + * computed: { + * ...mapVuexModuleState(vm => vm.vuexModule, ['foo']), + * } + * ``` + * + * @param {Function} namespaceSelector + * @param {Array|Object} fields + */ +export const mapVuexModuleState = (namespaceSelector, fields) => + mapVuexModuleFields({ + namespaceSelector, + fields, + vuexHelper: mapState, + selector: (namespace, value, state) => state[namespace][value], + }); + +/** + * Like `mapActions`, but takes a function in the first param for selecting a namespace. + * + * ``` + * methods: { + * ...mapVuexModuleActions(vm => vm.vuexModule, ['fetchFoos']), + * } + * ``` + * + * @param {Function} namespaceSelector + * @param {Array|Object} fields + */ +export const mapVuexModuleActions = (namespaceSelector, fields) => + mapVuexModuleFields({ + namespaceSelector, + fields, + vuexHelper: mapActions, + selector: (namespace, value, dispatch, ...args) => dispatch(`${namespace}/${value}`, ...args), + }); + +/** + * Like `mapGetters`, but takes a function in the first param for selecting a namespace. + * + * ``` + * computed: { + * ...mapGetters(vm => vm.vuexModule, ['hasSearchInfo']), + * } + * ``` + * + * @param {Function} namespaceSelector + * @param {Array|Object} fields + */ +export const mapVuexModuleGetters = (namespaceSelector, fields) => + mapVuexModuleFields({ + namespaceSelector, + fields, + // `mapGetters` does not let us pass an object which maps to functions. Thankfully `mapState` does + // and gives us access to the getters. + vuexHelper: mapState, + selector: (namespace, value, state, getters) => getters[`${namespace}/${value}`], + }); |