summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/lib
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 09:55:51 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 09:55:51 +0000
commite8d2c2579383897a1dd7f9debd359abe8ae8373d (patch)
treec42be41678c2586d49a75cabce89322082698334 /app/assets/javascripts/lib
parentfc845b37ec3a90aaa719975f607740c22ba6a113 (diff)
downloadgitlab-ce-e8d2c2579383897a1dd7f9debd359abe8ae8373d.tar.gz
Add latest changes from gitlab-org/gitlab@14-1-stable-eev14.1.0-rc42
Diffstat (limited to 'app/assets/javascripts/lib')
-rw-r--r--app/assets/javascripts/lib/dompurify.js11
-rw-r--r--app/assets/javascripts/lib/graphql.js52
-rw-r--r--app/assets/javascripts/lib/utils/axios_utils.js3
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js139
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/lib/utils/datetime/timeago_utility.js47
-rw-r--r--app/assets/javascripts/lib/utils/finite_state_machine.js101
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js58
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js76
9 files changed, 339 insertions, 149 deletions
diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js
index 76624c81ed5..4357918672d 100644
--- a/app/assets/javascripts/lib/dompurify.js
+++ b/app/assets/javascripts/lib/dompurify.js
@@ -7,6 +7,8 @@ const defaultConfig = {
ADD_TAGS: ['use'],
};
+const forbiddenDataAttrs = ['data-remote', 'data-url', 'data-type', 'data-method'];
+
// Only icons urls from `gon` are allowed
const getAllowedIconUrls = (gon = window.gon) =>
[gon.sprite_file_icons, gon.sprite_icons].filter(Boolean);
@@ -44,10 +46,19 @@ const sanitizeSvgIcon = (node) => {
removeUnsafeHref(node, 'xlink:href');
};
+const sanitizeHTMLAttributes = (node) => {
+ forbiddenDataAttrs.forEach((attr) => {
+ if (node.hasAttribute(attr)) {
+ node.removeAttribute(attr);
+ }
+ });
+};
+
addHook('afterSanitizeAttributes', (node) => {
if (node.tagName.toLowerCase() === 'use') {
sanitizeSvgIcon(node);
}
+ sanitizeHTMLAttributes(node);
});
export const sanitize = (val, config = defaultConfig) => dompurifySanitize(val, config);
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index cec689a44ca..0804213cafa 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -2,12 +2,13 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { BatchHttpLink } from 'apollo-link-batch-http';
-import { createHttpLink } from 'apollo-link-http';
+import { HttpLink } from 'apollo-link-http';
import { createUploadLink } from 'apollo-upload-client';
import ActionCableLink from '~/actioncable_link';
import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link';
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
import csrf from '~/lib/utils/csrf';
+import { objectToQuery, queryToObject } from '~/lib/utils/url_utility';
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
export const fetchPolicies = {
@@ -18,6 +19,31 @@ export const fetchPolicies = {
CACHE_ONLY: 'cache-only',
};
+export const stripWhitespaceFromQuery = (url, path) => {
+ /* eslint-disable-next-line no-unused-vars */
+ const [_, params] = url.split(path);
+
+ if (!params) {
+ return url;
+ }
+
+ const decoded = decodeURIComponent(params);
+ const paramsObj = queryToObject(decoded);
+
+ if (!paramsObj.query) {
+ return url;
+ }
+
+ const stripped = paramsObj.query
+ .split(/\s+|\n/)
+ .join(' ')
+ .trim();
+ paramsObj.query = stripped;
+
+ const reassembled = objectToQuery(paramsObj);
+ return `${path}?${reassembled}`;
+};
+
export default (resolvers = {}, config = {}) => {
const {
assumeImmutableResults,
@@ -58,10 +84,31 @@ export default (resolvers = {}, config = {}) => {
});
});
+ /*
+ This custom fetcher intervention is to deal with an issue where we are using GET to access
+ eTag polling, but Apollo Client adds excessive whitespace, which causes the
+ request to fail on certain self-hosted stacks. When we can move
+ to subscriptions entirely or can land an upstream PR, this can be removed.
+
+ Related links
+ Bug report: https://gitlab.com/gitlab-org/gitlab/-/issues/329895
+ Moving to subscriptions: https://gitlab.com/gitlab-org/gitlab/-/issues/332485
+ Apollo Client issue: https://github.com/apollographql/apollo-feature-requests/issues/182
+ */
+
+ const fetchIntervention = (url, options) => {
+ return fetch(stripWhitespaceFromQuery(url, uri), options);
+ };
+
+ const requestLink = ApolloLink.split(
+ () => useGet,
+ new HttpLink({ ...httpOptions, fetch: fetchIntervention }),
+ new BatchHttpLink(httpOptions),
+ );
+
const uploadsLink = ApolloLink.split(
(operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
createUploadLink(httpOptions),
- useGet ? createHttpLink(httpOptions) : new BatchHttpLink(httpOptions),
);
const performanceBarLink = new ApolloLink((operation, forward) => {
@@ -99,6 +146,7 @@ export default (resolvers = {}, config = {}) => {
new StartupJSLink(),
apolloCaptchaLink,
uploadsLink,
+ requestLink,
]),
);
diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js
index 204c84b879e..0a26f78e253 100644
--- a/app/assets/javascripts/lib/utils/axios_utils.js
+++ b/app/assets/javascripts/lib/utils/axios_utils.js
@@ -1,4 +1,5 @@
import axios from 'axios';
+import { registerCaptchaModalInterceptor } from '~/captcha/captcha_modal_axios_interceptor';
import setupAxiosStartupCalls from './axios_startup_calls';
import csrf from './csrf';
import suppressAjaxErrorsDuringNavigation from './suppress_ajax_errors_during_navigation';
@@ -41,6 +42,8 @@ axios.interceptors.response.use(
(err) => suppressAjaxErrorsDuringNavigation(err, isUserNavigating),
);
+registerCaptchaModalInterceptor(axios);
+
export default axios;
/**
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 8666d325c1b..8a051041fbe 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -11,31 +11,10 @@ import { isObject } from './type_utility';
import { getLocationHash } from './url_utility';
export const getPagePath = (index = 0) => {
- const page = $('body').attr('data-page') || '';
-
+ const { page = '' } = document?.body?.dataset;
return page.split(':')[index];
};
-export const getDashPath = (path = window.location.pathname) => path.split('/-/')[1] || null;
-
-export const isInGroupsPage = () => getPagePath() === 'groups';
-
-export const isInProjectPage = () => getPagePath() === 'projects';
-
-export const getProjectSlug = () => {
- if (isInProjectPage()) {
- return $('body').data('project');
- }
- return null;
-};
-
-export const getGroupSlug = () => {
- if (isInProjectPage() || isInGroupsPage()) {
- return $('body').data('group');
- }
- return null;
-};
-
export const checkPageAndAction = (page, action) => {
const pagePath = getPagePath(1);
const actionPath = getPagePath(2);
@@ -49,6 +28,8 @@ export const isInDesignPage = () => checkPageAndAction('issues', 'designs');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
+export const getDashPath = (path = window.location.pathname) => path.split('/-/')[1] || null;
+
export const getCspNonceValue = () => {
const metaTag = document.querySelector('meta[name=csp-nonce]');
return metaTag && metaTag.content;
@@ -162,53 +143,6 @@ export const parseUrlPathname = (url) => {
return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`;
};
-const splitPath = (path = '') => path.replace(/^\?/, '').split('&');
-
-export const urlParamsToArray = (path = '') =>
- splitPath(path)
- .filter((param) => param.length > 0)
- .map((param) => {
- const split = param.split('=');
- return [decodeURI(split[0]), split[1]].join('=');
- });
-
-export const getUrlParamsArray = () => urlParamsToArray(window.location.search);
-
-/**
- * Accepts encoding string which includes query params being
- * sent to URL.
- *
- * @param {string} path Query param string
- *
- * @returns {object} Query params object containing key-value pairs
- * with both key and values decoded into plain string.
- */
-export const urlParamsToObject = (path = '') =>
- splitPath(path).reduce((dataParam, filterParam) => {
- if (filterParam === '') {
- return dataParam;
- }
-
- const data = dataParam;
- let [key, value] = filterParam.split('=');
- key = /%\w+/g.test(key) ? decodeURIComponent(key) : key;
- const isArray = key.includes('[]');
- key = key.replace('[]', '');
- value = decodeURIComponent(value.replace(/\+/g, ' '));
-
- if (isArray) {
- if (!data[key]) {
- data[key] = [];
- }
-
- data[key].push(value);
- } else {
- data[key] = value;
- }
-
- return data;
- }, {});
-
export const isMetaKey = (e) => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// Identify following special clicks
@@ -301,21 +235,6 @@ export const debounceByAnimationFrame = (fn) => {
};
};
-/**
- this will take in the `name` of the param you want to parse in the url
- if the name does not exist this function will return `null`
- otherwise it will return the value of the param key provided
-*/
-export const getParameterByName = (name, urlToParse) => {
- const url = urlToParse || window.location.href;
- const parsedName = name.replace(/[[\]]/g, '\\$&');
- const regex = new RegExp(`[?&]${parsedName}(=([^&#]*)|&|#|$)`);
- const results = regex.exec(url);
- if (!results) return null;
- if (!results[2]) return '';
- return decodeURIComponent(results[2].replace(/\+/g, ' '));
-};
-
const handleSelectedRange = (range, restrictToNode) => {
// Make sure this range is within the restricting container
if (restrictToNode && !range.intersectsNode(restrictToNode)) return null;
@@ -390,8 +309,8 @@ export const insertText = (target, text) => {
};
/**
- this will take in the headers from an API response and normalize them
- this way we don't run into production issues when nginx gives us lowercased header keys
+ this will take in the headers from an API response and normalize them
+ this way we don't run into production issues when nginx gives us lowercased header keys
*/
export const normalizeHeaders = (headers) => {
const upperCaseHeaders = {};
@@ -418,39 +337,6 @@ export const parseIntPagination = (paginationInformation) => ({
previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
});
-/**
- * Given a string of query parameters creates an object.
- *
- * @example
- * `scope=all&page=2` -> { scope: 'all', page: '2'}
- * `scope=all` -> { scope: 'all' }
- * ``-> {}
- * @param {String} query
- * @returns {Object}
- */
-export const parseQueryStringIntoObject = (query = '') => {
- if (query === '') return {};
-
- return query.split('&').reduce((acc, element) => {
- const val = element.split('=');
- Object.assign(acc, {
- [val[0]]: decodeURIComponent(val[1]),
- });
- return acc;
- }, {});
-};
-
-/**
- * Converts object with key-value pairs
- * into query-param string
- *
- * @param {Object} params
- */
-export const objectToQueryString = (params = {}) =>
- Object.keys(params)
- .map((param) => `${param}=${params[param]}`)
- .join('&');
-
export const buildUrlWithCurrentLocation = (param) => {
if (param) return `${window.location.pathname}${param}`;
@@ -789,7 +675,18 @@ export const searchBy = (query = '', searchSpace = {}) => {
* @param {Object} label
* @returns Boolean
*/
-export const isScopedLabel = ({ title = '' }) => title.indexOf('::') !== -1;
+export const isScopedLabel = ({ title = '' } = {}) => title.indexOf('::') !== -1;
+
+/**
+ * Returns the base value of the scoped label
+ *
+ * Expected Label to be an Object with `title` as a key:
+ * { title: 'LabelTitle', ...otherProperties };
+ *
+ * @param {Object} label
+ * @returns String
+ */
+export const scopedLabelKey = ({ title = '' }) => isScopedLabel({ title }) && title.split('::')[0];
// Methods to set and get Cookie
export const setCookie = (name, value) => Cookies.set(name, value, { expires: 365 });
@@ -821,3 +718,5 @@ export const isFeatureFlagEnabled = (flag) => window.gon.features?.[flag];
* @returns {Array[String]} Converted array
*/
export const convertArrayToCamelCase = (array) => array.map((i) => convertToCamelCase(i));
+
+export const isLoggedIn = () => Boolean(window.gon?.current_user_id);
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 2d4765f54b9..e41de72ded4 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -1,4 +1,5 @@
export const BYTES_IN_KIB = 1024;
+export const DEFAULT_DEBOUNCE_AND_THROTTLE_MS = 250;
export const HIDDEN_CLASS = 'hidden';
export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
index 512b1f079a1..d68682ebed1 100644
--- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
@@ -1,10 +1,7 @@
-import $ from 'jquery';
import * as timeago from 'timeago.js';
-import { languageCode, s__ } from '../../../locale';
+import { languageCode, s__, createDateTimeFormat } from '../../../locale';
import { formatDate } from './date_format_utility';
-window.timeago = timeago;
-
/**
* Timeago uses underscores instead of dashes to separate language from country code.
*
@@ -76,24 +73,44 @@ const memoizedLocale = () => {
timeago.register(timeagoLanguageCode, memoizedLocale());
timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining());
-export const getTimeago = () => timeago;
+let memoizedFormatter = null;
+
+function setupAbsoluteFormatter() {
+ if (memoizedFormatter === null) {
+ const formatter = createDateTimeFormat({
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ });
+
+ memoizedFormatter = {
+ format(date) {
+ return formatter.format(date instanceof Date ? date : new Date(date));
+ },
+ };
+ }
+ return memoizedFormatter;
+}
+
+export const getTimeago = () =>
+ window.gon?.time_display_relative === false ? setupAbsoluteFormatter() : timeago;
/**
* For the given elements, sets a tooltip with a formatted date.
- * @param {JQuery} $timeagoEls
- * @param {Boolean} setTimeago
+ * @param {Array<Node>|NodeList} elements
+ * @param {Boolean} updateTooltip
*/
-export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
- $timeagoEls.each((i, el) => {
- $(el).text(timeago.format($(el).attr('datetime'), timeagoLanguageCode));
+export const localTimeAgo = (elements, updateTooltip = true) => {
+ const { format } = getTimeago();
+ elements.forEach((el) => {
+ el.innerText = format(el.dateTime, timeagoLanguageCode);
});
- if (!setTimeago) {
+ if (!updateTooltip) {
return;
}
function addTimeAgoTooltip() {
- $timeagoEls.each((i, el) => {
+ elements.forEach((el) => {
// Recreate with custom template
el.setAttribute('title', formatDate(el.dateTime));
});
@@ -116,9 +133,3 @@ export const timeFor = (time, expiredLabel) => {
}
return timeago.format(time, `${timeagoLanguageCode}-remaining`).trim();
};
-
-window.gl = window.gl || {};
-window.gl.utils = {
- ...(window.gl.utils || {}),
- localTimeAgo,
-};
diff --git a/app/assets/javascripts/lib/utils/finite_state_machine.js b/app/assets/javascripts/lib/utils/finite_state_machine.js
new file mode 100644
index 00000000000..99eeb7cb947
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/finite_state_machine.js
@@ -0,0 +1,101 @@
+/**
+ * @module finite_state_machine
+ */
+
+/**
+ * The states to be used with state machine definitions
+ * @typedef {Object} FiniteStateMachineStates
+ * @property {!Object} ANY_KEY - Any key that maps to a known state
+ * @property {!Object} ANY_KEY.on - A dictionary of transition events for the ANY_KEY state that map to a different state
+ * @property {!String} ANY_KEY.on.ANY_EVENT - The resulting state that the machine should end at
+ */
+
+/**
+ * An object whose minimum definition defined here can be used to guard UI state transitions
+ * @typedef {Object} StatelessFiniteStateMachineDefinition
+ * @property {FiniteStateMachineStates} states
+ */
+
+/**
+ * An object whose minimum definition defined here can be used to create a live finite state machine
+ * @typedef {Object} LiveFiniteStateMachineDefinition
+ * @property {String} initial - The initial state for this machine
+ * @property {FiniteStateMachineStates} states
+ */
+
+/**
+ * An object that allows interacting with a stateful, live finite state machine
+ * @typedef {Object} LiveStateMachine
+ * @property {String} value - The current state of this machine
+ * @property {Object} states - The states from when the machine definition was constructed
+ * @property {Function} is - {@link module:finite_state_machine~is LiveStateMachine.is}
+ * @property {Function} send - {@link module:finite_state_machine~send LiveStatemachine.send}
+ */
+
+// This is not user-facing functionality
+/* eslint-disable @gitlab/require-i18n-strings */
+
+function hasKeys(object, keys) {
+ return keys.every((key) => Object.keys(object).includes(key));
+}
+
+/**
+ * Get an updated state given a machine definition, a starting state, and a transition event
+ * @param {StatelessFiniteStateMachineDefinition} definition
+ * @param {String} current - The current known state
+ * @param {String} event - A transition event
+ * @returns {String} A state value
+ */
+export function transition(definition, current, event) {
+ return definition?.states?.[current]?.on[event] || current;
+}
+
+function startMachine({ states, initial } = {}) {
+ let current = initial;
+
+ return {
+ /**
+ * A convenience function to test arbitrary input against the machine's current state
+ * @param {String} testState - The value to test against the machine's current state
+ */
+ is(testState) {
+ return current === testState;
+ },
+ /**
+ * A function to transition the live state machine using an arbitrary event
+ * @param {String} event - The event to send to the machine
+ * @returns {String} A string representing the current state. Note this may not have changed if the current state + transition event combination are not valid.
+ */
+ send(event) {
+ current = transition({ states }, current, event);
+
+ return current;
+ },
+ get value() {
+ return current;
+ },
+ set value(forcedState) {
+ current = forcedState;
+ },
+ states,
+ };
+}
+
+/**
+ * Create a live state machine
+ * @param {LiveFiniteStateMachineDefinition} definition
+ * @returns {LiveStateMachine} A live state machine
+ */
+export function machine(definition) {
+ if (!hasKeys(definition, ['initial', 'states'])) {
+ throw new Error(
+ 'A state machine must have an initial state (`.initial`) and a dictionary of possible states (`.states`)',
+ );
+ } else if (!hasKeys(definition.states, [definition.initial])) {
+ throw new Error(
+ `Cannot initialize the state machine to state '${definition.initial}'. Is that one of the machine's defined states?`,
+ );
+ } else {
+ return startMachine(definition);
+ }
+}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index eaf396a7a59..5ee00464a8b 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -421,3 +421,61 @@ export const isValidSha1Hash = (str) => {
export function insertFinalNewline(content, endOfLine = '\n') {
return content.slice(-endOfLine.length) !== endOfLine ? `${content}${endOfLine}` : content;
}
+
+export const markdownConfig = {
+ // allowedTags from GitLab's inline HTML guidelines
+ // https://docs.gitlab.com/ee/user/markdown.html#inline-html
+ ALLOWED_TAGS: [
+ 'a',
+ 'abbr',
+ 'b',
+ 'blockquote',
+ 'br',
+ 'code',
+ 'dd',
+ 'del',
+ 'div',
+ 'dl',
+ 'dt',
+ 'em',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'hr',
+ 'i',
+ 'img',
+ 'ins',
+ 'kbd',
+ 'li',
+ 'ol',
+ 'p',
+ 'pre',
+ 'q',
+ 'rp',
+ 'rt',
+ 'ruby',
+ 's',
+ 'samp',
+ 'span',
+ 'strike',
+ 'strong',
+ 'sub',
+ 'summary',
+ 'sup',
+ 'table',
+ 'tbody',
+ 'td',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'tr',
+ 'tt',
+ 'ul',
+ 'var',
+ ],
+ ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
+ ALLOW_DATA_ATTR: false,
+};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index d68b41b7f7a..7922ff22a70 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -209,11 +209,7 @@ export function removeParams(params, url = window.location.href, skipEncoding =
return `${root}${writableQuery}${writableFragment}`;
}
-export function getLocationHash(url = window.location.href) {
- const hashIndex = url.indexOf('#');
-
- return hashIndex === -1 ? null : url.substring(hashIndex + 1);
-}
+export const getLocationHash = (hash = window.location.hash) => hash.split('#')[1];
/**
* Returns a boolean indicating whether the URL hash contains the given string value
@@ -409,6 +405,55 @@ export function getWebSocketUrl(path) {
return `${getWebSocketProtocol()}//${joinPaths(window.location.host, path)}`;
}
+const splitPath = (path = '') => path.replace(/^\?/, '').split('&');
+
+export const urlParamsToArray = (path = '') =>
+ splitPath(path)
+ .filter((param) => param.length > 0)
+ .map((param) => {
+ const split = param.split('=');
+ return [decodeURI(split[0]), split[1]].join('=');
+ });
+
+export const getUrlParamsArray = () => urlParamsToArray(window.location.search);
+
+/**
+ * Accepts encoding string which includes query params being
+ * sent to URL.
+ *
+ * @param {string} path Query param string
+ *
+ * @returns {object} Query params object containing key-value pairs
+ * with both key and values decoded into plain string.
+ *
+ * @deprecated Please use `queryToObject(query, { gatherArrays: true });` instead. See https://gitlab.com/gitlab-org/gitlab/-/issues/328845
+ */
+export const urlParamsToObject = (path = '') =>
+ splitPath(path).reduce((dataParam, filterParam) => {
+ if (filterParam === '') {
+ return dataParam;
+ }
+
+ const data = dataParam;
+ let [key, value] = filterParam.split('=');
+ key = /%\w+/g.test(key) ? decodeURIComponent(key) : key;
+ const isArray = key.includes('[]');
+ key = key.replace('[]', '');
+ value = decodeURIComponent(value.replace(/\+/g, ' '));
+
+ if (isArray) {
+ if (!data[key]) {
+ data[key] = [];
+ }
+
+ data[key].push(value);
+ } else {
+ data[key] = value;
+ }
+
+ return data;
+ }, {});
+
/**
* Convert search query into an object
*
@@ -450,17 +495,30 @@ export function queryToObject(query, { gatherArrays = false, legacySpacesDecode
}
/**
+ * This function accepts the `name` of the param to parse in the url
+ * if the name does not exist this function will return `null`
+ * otherwise it will return the value of the param key provided
+ *
+ * @param {String} name
+ * @param {String?} urlToParse
+ * @returns value of the parameter as string
+ */
+export const getParameterByName = (name, query = window.location.search) => {
+ return queryToObject(query)[name] || null;
+};
+
+/**
* Convert search query object back into a search query
*
- * @param {Object} obj that needs to be converted
+ * @param {Object?} params that needs to be converted
* @returns {String}
*
* ex: {one: 1, two: 2} into "one=1&two=2"
*
*/
-export function objectToQuery(obj) {
- return Object.keys(obj)
- .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`)
+export function objectToQuery(params = {}) {
+ return Object.keys(params)
+ .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
.join('&');
}