summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/lib/utils
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/lib/utils')
-rw-r--r--app/assets/javascripts/lib/utils/color_utils.js12
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js13
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js76
-rw-r--r--app/assets/javascripts/lib/utils/forms.js94
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js4
-rw-r--r--app/assets/javascripts/lib/utils/webpack.js6
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
}