diff options
Diffstat (limited to 'app/assets/javascripts/lib/utils')
-rw-r--r-- | app/assets/javascripts/lib/utils/color_utils.js | 12 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/common_utils.js | 13 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/datetime_utility.js | 76 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/forms.js | 94 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/text_markdown.js | 4 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/webpack.js | 6 |
6 files changed, 152 insertions, 53 deletions
diff --git a/app/assets/javascripts/lib/utils/color_utils.js b/app/assets/javascripts/lib/utils/color_utils.js index ff176f11867..da2c10076b1 100644 --- a/app/assets/javascripts/lib/utils/color_utils.js +++ b/app/assets/javascripts/lib/utils/color_utils.js @@ -43,3 +43,15 @@ export const validateHexColor = (color = '') => { return /^#([0-9A-F]{3}){1,2}$/i.test(color); }; + +export function darkModeEnabled() { + const ideDarkThemes = ['dark', 'solarized-dark', 'monokai']; + + // eslint-disable-next-line @gitlab/require-i18n-strings + const isWebIde = document.body.dataset.page.startsWith('ide:'); + + if (isWebIde) { + return ideDarkThemes.includes(window.gon?.user_color_scheme); + } + return document.body.classList.contains('gl-dark'); +} diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 73eadfe3cbe..fb257228597 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -681,6 +681,19 @@ export const roundOffFloat = (number, precision = 0) => { }; /** + * Method to round values to the nearest half (0.5) + * + * Eg; roundToNearestHalf(3.141592) = 3, roundToNearestHalf(3.41592) = 3.5 + * + * Refer to spec/javascripts/lib/utils/common_utils_spec.js for + * more supported examples. + * + * @param {Float} number + * @returns {Float|Number} + */ +export const roundToNearestHalf = (num) => Math.round(num * 2).toFixed() / 2; + +/** * Method to round down values with decimal places * with provided precision. * diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 145b419f8f0..a509828815a 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -4,8 +4,6 @@ import { isString, mapValues, isNumber, reduce } from 'lodash'; import * as timeago from 'timeago.js'; import { languageCode, s__, __, n__ } from '../../locale'; -const MILLISECONDS_IN_HOUR = 60 * 60 * 1000; -const MILLISECONDS_IN_DAY = 24 * MILLISECONDS_IN_HOUR; const DAYS_IN_WEEK = 7; window.timeago = timeago; @@ -256,6 +254,37 @@ export const timeIntervalInWords = (intervalInSeconds) => { : secondsText; }; +/** + * Similar to `timeIntervalInWords`, but rounds the return value + * to 1/10th of the largest time unit. For example: + * + * 30 => 30 seconds + * 90 => 1.5 minutes + * 7200 => 2 hours + * 86400 => 1 day + * ... etc. + * + * The largest supported unit is "days". + * + * @param {Number} intervalInSeconds The time interval in seconds + * @returns {String} A humanized description of the time interval + */ +export const humanizeTimeInterval = (intervalInSeconds) => { + if (intervalInSeconds < 60 /* = 1 minute */) { + const seconds = Math.round(intervalInSeconds * 10) / 10; + return n__('%d second', '%d seconds', seconds); + } else if (intervalInSeconds < 3600 /* = 1 hour */) { + const minutes = Math.round(intervalInSeconds / 6) / 10; + return n__('%d minute', '%d minutes', minutes); + } else if (intervalInSeconds < 86400 /* = 1 day */) { + const hours = Math.round(intervalInSeconds / 360) / 10; + return n__('%d hour', '%d hours', hours); + } + + const days = Math.round(intervalInSeconds / 8640) / 10; + return n__('%d day', '%d days', days); +}; + export const dateInWords = (date, abbreviated = false, hideYear = false) => { if (!date) return date; @@ -947,49 +976,6 @@ export const format24HourTimeStringFromInt = (time) => { }; /** - * A utility function which checks if two date ranges overlap. - * - * @param {Object} givenPeriodLeft - the first period to compare. - * @param {Object} givenPeriodRight - the second period to compare. - * @returns {Object} { daysOverlap: number of days the overlap is present, hoursOverlap: number of hours the overlap is present, overlapStartDate: the start date of the overlap in time format, overlapEndDate: the end date of the overlap in time format } - * @throws {Error} Uncaught Error: Invalid period - * - * @example - * getOverlapDateInPeriods( - * { start: new Date(2021, 0, 11), end: new Date(2021, 0, 13) }, - * { start: new Date(2021, 0, 11), end: new Date(2021, 0, 14) } - * ) => { daysOverlap: 2, hoursOverlap: 48, overlapStartDate: 1610323200000, overlapEndDate: 1610496000000 } - * - */ -export const getOverlapDateInPeriods = (givenPeriodLeft = {}, givenPeriodRight = {}) => { - const leftStartTime = new Date(givenPeriodLeft.start).getTime(); - const leftEndTime = new Date(givenPeriodLeft.end).getTime(); - const rightStartTime = new Date(givenPeriodRight.start).getTime(); - const rightEndTime = new Date(givenPeriodRight.end).getTime(); - - if (!(leftStartTime <= leftEndTime && rightStartTime <= rightEndTime)) { - throw new Error(__('Invalid period')); - } - - const isOverlapping = leftStartTime < rightEndTime && rightStartTime < leftEndTime; - - if (!isOverlapping) { - return { daysOverlap: 0 }; - } - - const overlapStartDate = Math.max(leftStartTime, rightStartTime); - const overlapEndDate = rightEndTime > leftEndTime ? leftEndTime : rightEndTime; - const differenceInMs = overlapEndDate - overlapStartDate; - - return { - hoursOverlap: Math.ceil(differenceInMs / MILLISECONDS_IN_HOUR), - daysOverlap: Math.ceil(differenceInMs / MILLISECONDS_IN_DAY), - overlapStartDate, - overlapEndDate, - }; -}; - -/** * A utility function that checks that the date is today * * @param {Date} date diff --git a/app/assets/javascripts/lib/utils/forms.js b/app/assets/javascripts/lib/utils/forms.js index 52e1323412d..b58aef15dda 100644 --- a/app/assets/javascripts/lib/utils/forms.js +++ b/app/assets/javascripts/lib/utils/forms.js @@ -1,3 +1,5 @@ +import { convertToCamelCase } from '~/lib/utils/text_utility'; + export const serializeFormEntries = (entries) => entries.reduce((acc, { name, value }) => Object.assign(acc, { [name]: value }), {}); @@ -51,3 +53,95 @@ export const serializeFormObject = (form) => return acc; }, []), ); + +/** + * Parse inputs of HTML forms generated by Rails. + * + * This can be helpful when mounting Vue components within Rails forms. + * + * If called with an HTML element like: + * + * ```html + * <input type="text" placeholder="Email" value="foo@bar.com" name="user[contact_info][email]" id="user_contact_info_email" data-js-name="contactInfoEmail"> + * <input type="text" placeholder="Phone" value="(123) 456-7890" name="user[contact_info][phone]" id="user_contact_info_phone" data-js-name="contactInfoPhone"> + * <input type="checkbox" name="user[interests][]" id="user_interests_vue" value="Vue" checked data-js-name="interests"> + * <input type="checkbox" name="user[interests][]" id="user_interests_graphql" value="GraphQL" data-js-name="interests"> + * ``` + * + * It will return an object like: + * + * ```javascript + * { + * contactInfoEmail: { + * name: 'user[contact_info][email]', + * id: 'user_contact_info_email', + * value: 'foo@bar.com', + * placeholder: 'Email', + * }, + * contactInfoPhone: { + * name: 'user[contact_info][phone]', + * id: 'user_contact_info_phone', + * value: '(123) 456-7890', + * placeholder: 'Phone', + * }, + * interests: [ + * { + * name: 'user[interests][]', + * id: 'user_interests_vue', + * value: 'Vue', + * checked: true, + * }, + * { + * name: 'user[interests][]', + * id: 'user_interests_graphql', + * value: 'GraphQL', + * checked: false, + * }, + * ], + * } + * ``` + * + * @param {HTMLInputElement} mountEl + * @returns {Object} object with form fields data. + */ +export const parseRailsFormFields = (mountEl) => { + if (!mountEl) { + throw new TypeError('`mountEl` argument is required'); + } + + const inputs = mountEl.querySelectorAll('[name]'); + + return [...inputs].reduce((accumulator, input) => { + const fieldName = input.dataset.jsName; + + if (!fieldName) { + return accumulator; + } + + const fieldNameCamelCase = convertToCamelCase(fieldName); + const { id, placeholder, name, value, type, checked } = input; + const attributes = { + name, + id, + value, + ...(placeholder && { placeholder }), + }; + + // Store radio buttons and checkboxes as an array so they can be + // looped through and rendered in Vue + if (['radio', 'checkbox'].includes(type)) { + return { + ...accumulator, + [fieldNameCamelCase]: [ + ...(accumulator[fieldNameCamelCase] || []), + { ...attributes, checked }, + ], + }; + } + + return { + ...accumulator, + [fieldNameCamelCase]: attributes, + }; + }, {}); +}; diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 345dfaf895b..1593a363dd1 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); + textToInsert = tag.replace(textPlaceholder, selected.replace(/\\n/g, '\n')); } else { textToInsert = String(startChar) + tag + selected + (wrap ? tag : ''); } @@ -322,7 +322,7 @@ export function updateTextForToolbarBtn($toolbarBtn) { blockTag: $toolbarBtn.data('mdBlock'), wrap: !$toolbarBtn.data('mdPrepend'), select: $toolbarBtn.data('mdSelect'), - tagContent: $toolbarBtn.data('mdTagContent'), + tagContent: $toolbarBtn.attr('data-md-tag-content'), }); } diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js index 07a4d2deb0b..a88f1bd82fc 100644 --- a/app/assets/javascripts/lib/utils/webpack.js +++ b/app/assets/javascripts/lib/utils/webpack.js @@ -11,10 +11,4 @@ export function resetServiceWorkersPublicPath() { const relativeRootPath = (gon && gon.relative_url_root) || ''; const webpackAssetPath = joinPaths(relativeRootPath, '/assets/webpack/'); __webpack_public_path__ = webpackAssetPath; // eslint-disable-line babel/camelcase - - // monaco-editor-webpack-plugin currently (incorrectly) references the - // public path as a property of `window`. Once this is fixed upstream we - // can remove this line - // see: https://github.com/Microsoft/monaco-editor-webpack-plugin/pull/63 - window.__webpack_public_path__ = webpackAssetPath; // eslint-disable-line } |