diff --git a/.eslintrc.js b/.eslintrc.js index f49834cc..b3a9bd5a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,6 +18,11 @@ module.exports = { 'rulesdir/no-multiple-api-calls': 'off', 'rulesdir/prefer-import-module-contents': 'off', 'no-constructor-return': 'off', + 'max-classes-per-file': 'off', + 'arrow-body-style': 'off', + 'es/no-nullish-coalescing-operators': 'off', + 'rulesdir/prefer-underscore-method': 'off', + 'es/no-optional-chaining': 'off', 'import/extensions': [ 'error', 'ignorePackages', diff --git a/__tests__/ExpensiMark-test.js b/__tests__/ExpensiMark-test.js index cad53935..fa7aa34b 100644 --- a/__tests__/ExpensiMark-test.js +++ b/__tests__/ExpensiMark-test.js @@ -1,6 +1,6 @@ /* eslint-disable max-len */ import ExpensiMark from '../lib/ExpensiMark'; -import _ from 'underscore'; +import * as Utils from '../lib/utils'; const parser = new ExpensiMark(); @@ -17,24 +17,28 @@ test('Test text is unescaped', () => { }); test('Test with regex Maximum regex stack depth reached error', () => { - const testString = '

heading

asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjcdidjjcdkekdiccjdkejdjcjxisdjjdkedncicdjejejcckdsijcjdsodjcicdkejdicdjejajasjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfisjksksjsjssskssjskskssksksksksskdkddkddkdksskskdkdkdksskskskdkdkdkdkekeekdkddenejeodxkdndekkdjddkeemdjxkdenendkdjddekjcjdkekejdcjdkeekcjcdidjjcdkekdiccjdkejdjcjxisdjjdkedncicdjejejcckdsijcjdsodjcicdkejdicdjejajasjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjcdidjjcdkekdiccjdkejdjcjxisdjjdkedncicdjejejcckdsijcjdsodjcicdkejdi.cdjd'; - const parser = new ExpensiMark(); + const testString = + '

heading

asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjcdidjjcdkekdiccjdkejdjcjxisdjjdkedncicdjejejcckdsijcjdsodjcicdkejdicdjejajasjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfisjksksjsjssskssjskskssksksksksskdkddkddkdksskskdkdkdksskskskdkdkdkdkekeekdkddenejeodxkdndekkdjddkeemdjxkdenendkdjddekjcjdkekejdcjdkeekcjcdidjjcdkekdiccjdkejdjcjxisdjjdkedncicdjejejcckdsijcjdsodjcicdkejdicdjejajasjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjcdidjjcdkekdiccjdkejdjcjxisdjjdkedncicdjejejcckdsijcjdsodjcicdkejdi.cdjd'; + const expensiMarkParser = new ExpensiMark(); // Mock method modifyTextForUrlLinks to let it throw an error to test try/catch of method ExpensiMark.replace - const modifyTextForUrlLinksMock = jest.fn((a, b, c) => {throw new Error('Maximum regex stack depth reached')}); - parser.modifyTextForUrlLinks = modifyTextForUrlLinksMock; - expect(parser.replace(testString)).toBe(_.escape(testString)); + const modifyTextForUrlLinksMock = jest.fn(() => { + throw new Error('Maximum regex stack depth reached'); + }); + expensiMarkParser.modifyTextForUrlLinks = modifyTextForUrlLinksMock; + expect(expensiMarkParser.replace(testString)).toBe(Utils.escape(testString)); expect(modifyTextForUrlLinksMock).toHaveBeenCalledTimes(1); // Mock method extractLinksInMarkdownComment to let it return undefined to test try/catch of method ExpensiMark.extractLinksInMarkdownComment - const extractLinksInMarkdownCommentMock = jest.fn((a) => undefined); - parser.extractLinksInMarkdownComment = extractLinksInMarkdownCommentMock; - expect(parser.extractLinksInMarkdownComment(testString)).toBe(undefined); - expect(parser.getRemovedMarkdownLinks(testString, 'google.com')).toStrictEqual([]); + const extractLinksInMarkdownCommentMock = jest.fn(() => undefined); + expensiMarkParser.extractLinksInMarkdownComment = extractLinksInMarkdownCommentMock; + expect(expensiMarkParser.extractLinksInMarkdownComment(testString)).toBe(undefined); + expect(expensiMarkParser.getRemovedMarkdownLinks(testString, 'google.com')).toStrictEqual([]); expect(extractLinksInMarkdownCommentMock).toHaveBeenCalledTimes(3); }); test('Test extract link with ending parentheses', () => { - const comment = '[Staging Detail](https://staging.new.expensify.com/details) [Staging Detail](https://staging.new.expensify.com/details)) [Staging Detail](https://staging.new.expensify.com/details)))'; + const comment = + '[Staging Detail](https://staging.new.expensify.com/details) [Staging Detail](https://staging.new.expensify.com/details)) [Staging Detail](https://staging.new.expensify.com/details)))'; const links = ['https://staging.new.expensify.com/details', 'https://staging.new.expensify.com/details', 'https://staging.new.expensify.com/details']; expect(parser.extractLinksInMarkdownComment(comment)).toStrictEqual(links); }); diff --git a/lib/API.jsx b/lib/API.jsx index 5a98186f..672eeb82 100644 --- a/lib/API.jsx +++ b/lib/API.jsx @@ -3,12 +3,12 @@ * WIP, This is in the process of migration from web-e. Please add methods to this as is needed.| * ---------------------------------------------------------------------------------------------- */ -import _ from 'underscore'; // Use this deferred lib so we don't have a dependency on jQuery (so we can use this module in mobile) import {Deferred} from 'simply-deferred'; +import {has} from 'lodash'; import ExpensifyAPIDeferred from './APIDeferred'; -import {isWindowAvailable} from './utils'; +import * as Utils from './utils'; /** * @param {Network} network @@ -40,7 +40,7 @@ export default function API(network, args) { * Returns a promise that is rejected if a change is detected * Otherwise, it is resolved successfully * - * @returns {Object} $.Deferred + * @returns {Object} Deferred */ function isRunningLatestVersionOfCode() { const promise = new Deferred(); @@ -48,7 +48,7 @@ export default function API(network, args) { network .get('/revision.txt') .done((codeRevision) => { - if (isWindowAvailable() && codeRevision.trim() === window.CODE_REVISION) { + if (Utils.isWindowAvailable() && codeRevision.trim() === window.CODE_REVISION) { console.debug('Code revision is up to date'); promise.resolve(); } else { @@ -75,7 +75,7 @@ export default function API(network, args) { * @param {String} apiDeferred */ function attachJSONCodeCallbacks(apiDeferred) { - _(defaultHandlers).each((callbacks, code) => { + Object.entries(defaultHandlers).forEach(([code, callbacks]) => { // The key, `code`, is returned as a string, so we must cast it to an Integer const jsonCode = parseInt(code, 10); callbacks.forEach((callback) => { @@ -105,7 +105,7 @@ export default function API(network, args) { let newParameters = {...parameters, command}; // If there was an enhanceParameters() method supplied in our args, then we will call that here - if (args && _.isFunction(args.enhanceParameters)) { + if (args && Utils.isFunction(args.enhanceParameters)) { newParameters = args.enhanceParameters(newParameters); } @@ -153,17 +153,19 @@ export default function API(network, args) { function requireParameters(parameterNames, parameters, commandName) { // eslint-disable-next-line rulesdir/prefer-early-return parameterNames.forEach((parameterName) => { - if (!_(parameters).has(parameterName) || parameters[parameterName] === null || parameters[parameterName] === undefined) { - const parametersCopy = _.clone(parameters); - if (_(parametersCopy).has('authToken')) { - parametersCopy.authToken = ''; - } - if (_(parametersCopy).has('password')) { - parametersCopy.password = ''; - } - const keys = _(parametersCopy).keys().join(', ') || 'none'; - throw new Error(`Parameter ${parameterName} is required for "${commandName}". Supplied parameters: ${keys}`); + if (has(parameters, parameterName) && parameters[parameterName] !== null && parameters[parameterName] !== undefined) { + return; + } + + const parametersCopy = {...parameters}; + if (has(parametersCopy, 'authToken')) { + parametersCopy.authToken = ''; + } + if (has(parametersCopy, 'password')) { + parametersCopy.password = ''; } + const keys = Object.keys(parametersCopy).join(', ') || 'none'; + throw new Error(`Parameter ${parameterName} is required for "${commandName}". Supplied parameters: ${keys}`); }); } @@ -173,7 +175,7 @@ export default function API(network, args) { * @param {Function} callback */ registerDefaultHandler(jsonCodes, callback) { - if (!_(callback).isFunction()) { + if (!Utils.isFunction(callback)) { return; } @@ -230,7 +232,7 @@ export default function API(network, args) { return (parameters, keepalive = false) => { // Optional validate function for required logic before making the call. e.g. validating params in the front-end etc. - if (_.isFunction(data.validate)) { + if (Utils.isFunction(data.validate)) { data.validate(parameters); } @@ -265,7 +267,7 @@ export default function API(network, args) { requireParameters(['email'], parameters, commandName); // Tell the API not to set cookies for this request - const newParameters = _.extend({api_setCookie: false}, parameters); + const newParameters = {...parameters, api_setCookie: false}; return performPOSTRequest(commandName, newParameters); }, @@ -426,7 +428,7 @@ export default function API(network, args) { const commandName = 'ResetPassword'; requireParameters(['email'], parameters, commandName); - const newParameters = _.extend({api_setCookie: false}, parameters); + const newParameters = {...parameters, api_setCookie: false}; return performPOSTRequest(commandName, newParameters); }, diff --git a/lib/APIDeferred.jsx b/lib/APIDeferred.jsx index c64b17b8..b95b966c 100644 --- a/lib/APIDeferred.jsx +++ b/lib/APIDeferred.jsx @@ -3,9 +3,9 @@ * WIP, This is in the process of migration from web-e. Please add methods to this as is needed.| * ---------------------------------------------------------------------------------------------- */ - -import _ from 'underscore'; -import {invoke, bulkInvoke} from './Func'; +import {once} from 'lodash'; +import * as Utils from './utils'; +import * as Func from './Func'; /** * @param {jquery.Deferred} promise @@ -50,15 +50,15 @@ export default function APIDeferred(promise, extractedProperty) { function handleError(jsonCode, response) { // Look for handlers for this error code const handlers = errorHandlers[jsonCode]; - if (!_(handlers).isEmpty()) { - bulkInvoke(handlers, [jsonCode, response]); + if (handlers.length > 0) { + Func.bulkInvoke(handlers, [jsonCode, response]); } else { // No explicit handlers, call the unhandled callbacks - bulkInvoke(unhandledCallbacks, [jsonCode, response]); + Func.bulkInvoke(unhandledCallbacks, [jsonCode, response]); } // Always run the "fail" callbacks in case of error - bulkInvoke(failCallbacks, [jsonCode, response]); + Func.bulkInvoke(failCallbacks, [jsonCode, response]); } /** @@ -73,8 +73,8 @@ export default function APIDeferred(promise, extractedProperty) { // Figure out if we need to extract a property from the response, and if it is there. const jsonCode = extractJSONCode(response); - const propertyRequested = !_.isNull(extractedPropertyName); - const requestedPropertyPresent = propertyRequested && response && !_.isUndefined(response[extractedPropertyName]); + const propertyRequested = !Number.isNull(extractedPropertyName); + const requestedPropertyPresent = propertyRequested && response && response[extractedPropertyName] !== undefined; const propertyRequestedButMissing = propertyRequested && !requestedPropertyPresent; // Save the response for any callbacks that might run in the future @@ -86,8 +86,8 @@ export default function APIDeferred(promise, extractedProperty) { returnedData = propertyRequested && requestedPropertyPresent ? response[extractedPropertyName] : response; // And then run the success callbacks - bulkInvoke(doneCallbacks, [returnedData]); - } else if (!_(jsonCode).isNull() && jsonCode !== 200) { + Func.bulkInvoke(doneCallbacks, [returnedData]); + } else if (jsonCode !== null && jsonCode !== 200) { // Exception thrown, handle it handleError(jsonCode, response); } else { @@ -102,7 +102,7 @@ export default function APIDeferred(promise, extractedProperty) { } // Always run the "always" callbacks - bulkInvoke(alwaysCallbacks, [response]); + Func.bulkInvoke(alwaysCallbacks, [response]); } /** @@ -133,8 +133,8 @@ export default function APIDeferred(promise, extractedProperty) { * @returns {APIDeferred} itself, for chaining */ done(callback) { - if (_(callback).isFunction()) { - doneCallbacks.push(_(callback).once()); + if (Utils.isFunction(callback)) { + doneCallbacks.push(once(callback)); ensureFutureCallbacksFire(); } return this; @@ -148,8 +148,8 @@ export default function APIDeferred(promise, extractedProperty) { * @returns {APIDeferred} itself, for chaining */ always(callback) { - if (_(callback).isFunction()) { - alwaysCallbacks.push(_(callback).once()); + if (Utils.isFunction(callback)) { + alwaysCallbacks.push(once(callback)); ensureFutureCallbacksFire(); } return this; @@ -165,7 +165,7 @@ export default function APIDeferred(promise, extractedProperty) { * @returns {APIDeferred} itself, for chaining */ handle(jsonCodes, callback) { - if (_(callback).isFunction()) { + if (Utils.isFunction(callback)) { jsonCodes.forEach((code) => { if (code === 200) { return; @@ -174,7 +174,7 @@ export default function APIDeferred(promise, extractedProperty) { if (!errorHandlers[code]) { errorHandlers[code] = []; } - errorHandlers[code].push(_(callback).once()); + errorHandlers[code].push(once(callback)); }); ensureFutureCallbacksFire(); @@ -191,8 +191,8 @@ export default function APIDeferred(promise, extractedProperty) { * @returns {APIDeferred} itself, for chaining */ unhandled(callback) { - if (_(callback).isFunction()) { - unhandledCallbacks.push(_(callback).once()); + if (Utils.isFunction(callback)) { + unhandledCallbacks.push(once(callback)); ensureFutureCallbacksFire(); } return this; @@ -207,8 +207,8 @@ export default function APIDeferred(promise, extractedProperty) { * @returns {APIDeferred} itself, for chaining */ fail(callback) { - if (_(callback).isFunction()) { - failCallbacks.push(_(callback).once()); + if (Utils.isFunction(callback)) { + failCallbacks.push(once(callback)); ensureFutureCallbacksFire(); } return this; @@ -225,11 +225,11 @@ export default function APIDeferred(promise, extractedProperty) { return promise.then((response) => { const responseCode = extractJSONCode(response); - if (responseCode !== 200 || !_(callback).isFunction()) { + if (responseCode !== 200 || !Utils.isFunction(callback)) { return; } - invoke(callback, [response]); + Func.invoke(callback, [response]); return this; }); diff --git a/lib/BrowserDetect.jsx b/lib/BrowserDetect.jsx index 63894f96..cb0731f8 100644 --- a/lib/BrowserDetect.jsx +++ b/lib/BrowserDetect.jsx @@ -1,4 +1,4 @@ -import {isNavigatorAvailable, isWindowAvailable} from './utils'; +import * as Utils from './utils'; const BROWSERS = { EDGE: 'Edge', @@ -15,7 +15,7 @@ const MOBILE_PLATFORMS = { }; function searchString() { - if (!isWindowAvailable() || !isNavigatorAvailable()) { + if (!Utils.isWindowAvailable() || !Utils.isNavigatorAvailable()) { return ''; } @@ -78,7 +78,7 @@ function searchString() { } function getMobileDevice() { - if (!isNavigatorAvailable() || !navigator.userAgent) { + if (!Utils.isNavigatorAvailable() || !navigator.userAgent) { return ''; } diff --git a/lib/ExpenseRule.jsx b/lib/ExpenseRule.jsx index 03c66be3..f2e89dd8 100644 --- a/lib/ExpenseRule.jsx +++ b/lib/ExpenseRule.jsx @@ -1,5 +1,3 @@ -import _ from 'underscore'; - export default class ExpenseRule { /** * Creates a new instance of this class. @@ -7,7 +5,7 @@ export default class ExpenseRule { * @param {Array} ruleArray */ constructor(ruleArray) { - _.each(ruleArray, (value, key) => { + ruleArray.forEach((value, key) => { this[key] = value; }); } @@ -21,7 +19,7 @@ export default class ExpenseRule { * @return {Object} */ getApplyWhenByField(field) { - return _.find(this.applyWhen, (conditions) => conditions.field === field) || {}; + return this.applyWhen.find((conditions) => conditions.field === field) || {}; } /** @@ -41,7 +39,7 @@ export default class ExpenseRule { */ isMatch(expense) { let isMatch = true; - _.each(this.applyWhen, (conditions) => { + this.applyWhen.forEach((conditions) => { switch (conditions.field) { case 'category': if (!this.checkCondition(conditions.condition, conditions.value, expense.getCategory())) { diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js index c1f437ff..6bc9981b 100644 --- a/lib/ExpensiMark.js +++ b/lib/ExpensiMark.js @@ -1,8 +1,8 @@ -import * as _ from 'underscore'; import Str from './str'; import * as Constants from './CONST'; import * as UrlPatterns from './Url'; import Logger from './Logger'; +import * as Utils from './utils'; const MARKDOWN_LINK_REGEX = new RegExp(`\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)]\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, 'gi'); const MARKDOWN_IMAGE_REGEX = new RegExp(`\\!(?:\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)])?\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, 'gi'); @@ -11,7 +11,7 @@ const SLACK_SPAN_NEW_LINE_TAG = ' {}, // eslint-disable-next-line no-console clientLoggingCallback: (message) => console.warn(message), isDebug: true, @@ -445,17 +445,12 @@ export default class ExpensiMark { .split('\n'); // Wrap each string in the array with
and
- // Define a named function to wrap each line with blockquote - function wrapWithBlockquote(line) { + resultString = resultString.map((line) => { return `
${line}
`; - } - - // Use _.map with the named function - resultString = _.map(resultString, wrapWithBlockquote); + }); - function processString(m) { - // Recursive function to replace nested
with ">" - function replaceBlockquotes(text) { + resultString = resultString + .map((text) => { let modifiedText = text; let depth; do { @@ -464,11 +459,8 @@ export default class ExpensiMark { modifiedText = modifiedText.replace(/<\/blockquote>/gi, ''); } while (/
/i.test(modifiedText)); return `${'>'.repeat(depth)} ${modifiedText}`; - } - return replaceBlockquotes(m); - } - - resultString = _.map(resultString, processString).join('\n'); + }) + .join('\n'); // We want to keep
tag here and let method replaceBlockElementWithNewLine to handle the line break later return `
${resultString}
`; @@ -621,13 +613,13 @@ export default class ExpensiMark { * @param {Object} rule - The rule to check. * @returns {boolean} Returns true if the rule should be applied, otherwise false. */ - this.filterRules = (rule) => !_.includes(this.whitespaceRulesToDisable, rule.name); + this.filterRules = (rule) => !this.whitespaceRulesToDisable.includes(rule.name); /** * Filters rules to determine which should keep whitespace. * @returns {Object[]} The filtered rules. */ - this.shouldKeepWhitespaceRules = _.filter(this.rules, this.filterRules); + this.shouldKeepWhitespaceRules = this.rules.filter(this.filterRules); /** * maxQuoteDepth is the maximum depth of nested quotes that we want to support. @@ -644,16 +636,16 @@ export default class ExpensiMark { getHtmlRuleset(filterRules, disabledRules, shouldKeepRawInput) { let rules = this.rules; - const hasRuleName = (rule) => _.contains(filterRules, rule.name); - const hasDisabledRuleName = (rule) => !_.contains(disabledRules, rule.name); + const hasRuleName = (rule) => filterRules.includes(rule.name); + const hasDisabledRuleName = (rule) => !disabledRules.includes(rule.name); if (shouldKeepRawInput) { rules = this.shouldKeepWhitespaceRules; } - if (!_.isEmpty(filterRules)) { - rules = _.filter(this.rules, hasRuleName); + if (filterRules.length > 0) { + rules = this.rules.filter(hasRuleName); } - if (!_.isEmpty(disabledRules)) { - rules = _.filter(rules, hasDisabledRuleName); + if (disabledRules.length > 0) { + rules = rules.filter(hasDisabledRuleName); } return rules; } @@ -673,7 +665,7 @@ export default class ExpensiMark { */ replace(text, {filterRules = [], shouldEscapeText = true, shouldKeepRawInput = false, disabledRules = []} = {}) { // This ensures that any html the user puts into the comment field shows as raw html - let replacedText = shouldEscapeText ? _.escape(text) : text; + let replacedText = shouldEscapeText ? Utils.escape(text) : text; const rules = this.getHtmlRuleset(filterRules, disabledRules, shouldKeepRawInput); const processRule = (rule) => { @@ -700,7 +692,7 @@ export default class ExpensiMark { console.warn('Error replacing text with html in ExpensiMark.replace', {error: e}); // We want to return text without applying rules if exception occurs during replacing - return shouldEscapeText ? _.escape(text) : text; + return shouldEscapeText ? Utils.escape(text) : text; } return replacedText; @@ -870,7 +862,7 @@ export default class ExpensiMark { /|<\/div>||\n<\/comment>|<\/comment>|

|<\/h1>|

|<\/h2>|

|<\/h3>|

|<\/h4>|

|<\/h5>|
|<\/h6>|

|<\/p>|

  • |<\/li>|
    |<\/blockquote>/, ); const stripHTML = (text) => Str.stripHTML(text); - splitText = _.map(splitText, stripHTML); + splitText = splitText.map(stripHTML); let joinedText = ''; // Delete whitespace at the end @@ -924,7 +916,7 @@ export default class ExpensiMark { } // if replacement is a function, we want to pass optional extras to it - const replacementFunction = typeof rule.replacement === 'function' ? (...args) => rule.replacement(...args, extras) : rule.replacement; + const replacementFunction = Utils.isFunction(rule.replacement) ? (...args) => rule.replacement(...args, extras) : rule.replacement; generatedMarkdown = generatedMarkdown.replace(rule.regex, replacementFunction); }; @@ -944,7 +936,7 @@ export default class ExpensiMark { let replacedText = htmlString; const processRule = (rule) => { // if replacement is a function, we want to pass optional extras to it - const replacementFunction = typeof rule.replacement === 'function' ? (...args) => rule.replacement(...args, extras) : rule.replacement; + const replacementFunction = Utils.isFunction(rule.replacement) ? (...args) => rule.replacement(...args, extras) : rule.replacement; replacedText = replacedText.replace(rule.regex, replacementFunction); }; @@ -1038,7 +1030,7 @@ export default class ExpensiMark { } return quoteContent; }; - let textToFormat = _.map(textToCheck.split('\n'), formatRow).join('\n'); + let textToFormat = textToCheck.split('\n').map(formatRow).join('\n'); // Remove leading and trailing line breaks textToFormat = textToFormat.replace(/^\n+|\n+$/g, ''); @@ -1099,7 +1091,7 @@ export default class ExpensiMark { // Element 1 from match is the regex group if it exists which contains the link URLs const sanitizeMatch = (match) => Str.sanitizeURL(match[1]); - const links = _.map(matches, sanitizeMatch); + const links = matches.map(sanitizeMatch); return links; } catch (e) { // eslint-disable-next-line no-console @@ -1118,7 +1110,7 @@ export default class ExpensiMark { getRemovedMarkdownLinks(oldComment, newComment) { const linksInOld = this.extractLinksInMarkdownComment(oldComment); const linksInNew = this.extractLinksInMarkdownComment(newComment); - return linksInOld === undefined || linksInNew === undefined ? [] : _.difference(linksInOld, linksInNew); + return linksInOld === undefined || linksInNew === undefined ? [] : linksInOld.filter((link) => !linksInNew.includes(link)); } /** @@ -1135,6 +1127,6 @@ export default class ExpensiMark { // When the attribute contains HTML and is converted back to MD we need to re-escape it to avoid // illegal attribute value characters like `," or ' which might break the HTML originalContent = Str.replaceAll(originalContent, '\n', ''); - return _.escape(originalContent); + return Utils.escape(originalContent); } } diff --git a/lib/Func.jsx b/lib/Func.jsx index 3a636b47..7d1ccefb 100644 --- a/lib/Func.jsx +++ b/lib/Func.jsx @@ -1,5 +1,4 @@ -import $ from 'jquery'; -import _ from 'underscore'; +import * as Utils from './utils'; /** * Invokes the given callback with the given arguments @@ -11,7 +10,7 @@ import _ from 'underscore'; * @returns {Mixed} */ function invoke(callback, args, scope) { - if (!_(callback).isFunction()) { + if (!Utils.isFunction(callback)) { return null; } @@ -26,18 +25,18 @@ function invoke(callback, args, scope) { * @param {Array} [args] * @param {Object} [scope] * - * @returns {$.Deferred} + * @returns {Promise} */ function invokeAsync(callback, args, scope) { - if (!_(callback).isFunction()) { - return new $.Deferred().resolve(); + if (!Utils.isFunction(callback)) { + return Promise.resolve(); } let promiseFromCallback = callback.apply(scope, args || []); // If there was not a promise returned from the prefetch callback, then create a dummy promise and resolve it if (!promiseFromCallback) { - promiseFromCallback = new $.Deferred().resolve(); + promiseFromCallback = Promise.resolve(); } return promiseFromCallback; @@ -68,7 +67,11 @@ function die() { * @returns {Array} */ function mapByName(list, methodName) { - return _.map(list, (item) => item[methodName].call(item)); + let arr = list; + if (!Array.isArray(arr)) { + arr = Object.values(arr); + } + return arr.map((item) => item[methodName].call(item)); } export {invoke, invokeAsync, bulkInvoke, die, mapByName}; diff --git a/lib/Log.jsx b/lib/Log.jsx index bd55a1f9..3c90d904 100644 --- a/lib/Log.jsx +++ b/lib/Log.jsx @@ -1,9 +1,8 @@ /* eslint-disable no-console */ -import _ from 'underscore'; import API from './API'; import Network from './Network'; import Logger from './Logger'; -import {isWindowAvailable} from './utils'; +import * as Utils from './utils'; /** * Network interface for logger. @@ -24,11 +23,11 @@ function serverLoggingCallback(logger, params) { * @param {String} message */ function clientLoggingCallback(message) { - if (isWindowAvailable() && typeof window.g_printableReport !== 'undefined' && window.g_printableReport === true) { + if (Utils.isWindowAvailable() && typeof window.g_printableReport !== 'undefined' && window.g_printableReport === true) { return; } - if (window.console && _.isFunction(console.log)) { + if (window.console && Utils.isFunction(console.log)) { console.log(message); } } @@ -36,5 +35,5 @@ function clientLoggingCallback(message) { export default new Logger({ serverLoggingCallback, clientLoggingCallback, - isDebug: isWindowAvailable() ? window.DEBUG : false, + isDebug: Utils.isWindowAvailable() ? window.DEBUG : false, }); diff --git a/lib/Logger.ts b/lib/Logger.ts index 90d70b9a..61756cf3 100644 --- a/lib/Logger.ts +++ b/lib/Logger.ts @@ -53,9 +53,10 @@ export default class Logger { } // eslint-disable-next-line rulesdir/prefer-early-return promise.then((response) => { - if (response.requestID) { - this.info('Previous log requestID', false, {requestID: response.requestID}, true); + if (!response.requestID) { + return; } + this.info('Previous log requestID', false, {requestID: response.requestID}, true); }); } diff --git a/lib/Network.jsx b/lib/Network.jsx index 85c6e432..7d109a2f 100644 --- a/lib/Network.jsx +++ b/lib/Network.jsx @@ -1,6 +1,5 @@ import $ from 'jquery'; -import _ from 'underscore'; -import {isWindowAvailable} from './utils'; +import * as Utils from './utils'; /** * Adds our API command to the URL so the API call is more easily identified in the @@ -41,7 +40,7 @@ export default function Network(endpoint) { } // Attach a listener to the event indicating that we're leaving a page - if (isWindowAvailable()) { + if (Utils.isWindowAvailable()) { window.onbeforeunload = () => { isNavigatingAway = true; }; @@ -87,7 +86,7 @@ export default function Network(endpoint) { // Check to see if parameters contains a File or Blob object // If it does, we should use formData instead of parameters and update // the ajax settings accordingly - _(parameters).each((value, key) => { + Object.entries(parameters).forEach(([key, value]) => { if (!value) { return; } @@ -133,14 +132,14 @@ export default function Network(endpoint) { // Add our data as form data const formData = new FormData(); - _(parameters).each((value, key) => { - if (_.isUndefined(value)) { + Object.entries(parameters).forEach(([key, value]) => { + if (value === undefined) { return; } - if (_.isArray(value)) { - _.each(value, (valueItem, i) => { - if (_.isObject(valueItem)) { - _.each(valueItem, (valueItemObjectValue, valueItemObjectKey) => { + if (Array.isArray(value)) { + value.forEach((valueItem, i) => { + if (Utils.isObject(valueItem)) { + Object.entries(valueItem).forEach(([valueItemObjectKey, valueItemObjectValue]) => { formData.append(`${key}[${i}][${valueItemObjectKey}]`, valueItemObjectValue); }); } else { diff --git a/lib/Num.jsx b/lib/Num.jsx index 82b864d9..80d83c1c 100644 --- a/lib/Num.jsx +++ b/lib/Num.jsx @@ -1,4 +1,3 @@ -import _ from 'underscore'; import Str from './str'; export default { @@ -123,7 +122,7 @@ export default { * @returns {Boolean} true if the number is finite and not NaN. */ isFiniteNumber(number) { - return _.isNumber(number) && _.isFinite(number) && !_.isNaN(number); + return typeof number === 'number' && Number.isFinite(number) && !Number.isNaN(number); }, /** diff --git a/lib/PubSub.jsx b/lib/PubSub.jsx index 28fc6ea7..aa0df607 100644 --- a/lib/PubSub.jsx +++ b/lib/PubSub.jsx @@ -1,7 +1,7 @@ -import _ from 'underscore'; import has from 'lodash/has'; +import {once} from 'lodash'; import Log from './Log'; -import {isWindowAvailable} from './utils'; +import * as Utils from './utils'; /** * PubSub @@ -46,14 +46,14 @@ const PubSubModule = { return; } - const eventIDs = _.keys(eventMap[eventName]); + const eventIDs = eventMap[eventName].keys(); if (eventName === this.ERROR) { // Doing the split slice 2 because the 1st element of the stacktrace will always be from here (PubSub.publish) // When debugging, we just need to know who called PubSub.publish (so, all next elements in the stack) Log.hmmm('Error published', 0, {tplt: param.tplt, stackTrace: new Error().stack.split(' at ').slice(2)}); } - _.each(eventIDs, (eventID) => { + eventIDs.forEach((eventID) => { const subscriber = eventMap[eventName][eventID]; if (subscriber) { subscriber.callback.call(subscriber.scope, param); @@ -72,8 +72,8 @@ const PubSubModule = { * @returns {String} */ once(eventName, callback, optionalScope) { - const scope = _.isObject(optionalScope) ? optionalScope : window; - const functionToCallOnce = _.once((...args) => callback.apply(scope, args)); + const scope = Utils.isObject(optionalScope) && optionalScope !== null ? optionalScope : window; + const functionToCallOnce = once((...args) => callback.apply(scope, args)); return this.subscribe(eventName, functionToCallOnce); }, @@ -94,8 +94,8 @@ const PubSubModule = { throw new Error('Attempted to subscribe to undefined event'); } - const callback = _.isFunction(optionalCallback) ? optionalCallback : () => {}; - const scope = _.isObject(optionalScope) ? optionalScope : window; + const callback = Utils.isFunction(optionalCallback) ? optionalCallback : () => {}; + const scope = Utils.isObject(optionalScope) && optionalScope !== null ? optionalScope : window; const eventID = generateID(eventName); if (eventMap[eventName] === undefined) { @@ -116,8 +116,8 @@ const PubSubModule = { * @param {String} bindID The id of the element to delete */ unsubscribe(bindID) { - const IDs = _.isArray(bindID) ? bindID : [bindID]; - _.each(IDs, (id) => { + const IDs = Array.isArray(bindID) ? bindID : [bindID]; + IDs.forEach((id) => { const eventName = extractEventName(id); if (has(eventMap, `${eventName}.${id}`)) { delete eventMap[eventName][id]; @@ -126,4 +126,4 @@ const PubSubModule = { }, }; -export default isWindowAvailable() && window.PubSub ? window.PubSub : PubSubModule; +export default Utils.isWindowAvailable() && window.PubSub ? window.PubSub : PubSubModule; diff --git a/lib/ReportHistoryStore.jsx b/lib/ReportHistoryStore.jsx index c8acae97..b92f694b 100644 --- a/lib/ReportHistoryStore.jsx +++ b/lib/ReportHistoryStore.jsx @@ -1,4 +1,3 @@ -import _ from 'underscore'; import {Deferred} from 'simply-deferred'; export default class ReportHistoryStore { @@ -31,11 +30,12 @@ export default class ReportHistoryStore { * * @returns {Object[]} */ - this.filterHiddenActions = (historyItems) => _.filter(historyItems, (historyItem) => historyItem.shouldShow); + this.filterHiddenActions = (historyItems) => historyItems.filter((historyItem) => historyItem.shouldShow); /** * Public Methods */ + return { /** * Returns the history for a given report. @@ -124,7 +124,7 @@ export default class ReportHistoryStore { this.getFromCache(reportID) .done((cachedHistory) => { // Do we have the reportAction immediately before this one? - if (_.some(cachedHistory, ({reportActionID}) => reportActionID === reportAction.reportActionID)) { + if (cachedHistory.some(({reportActionID}) => reportActionID === reportAction.reportActionID)) { // If we have the previous one then we can assume we have an up to date history minus the most recent // and must merge it into the cache this.mergeHistoryByTimestamp(reportID, [reportAction]); @@ -149,7 +149,7 @@ export default class ReportHistoryStore { * @param {String[]} events */ bindCacheClearingEvents: (events) => { - _.each(events, (event) => this.PubSub.subscribe(event, () => (this.cache = {}))); + events.each((event) => this.PubSub.subscribe(event, () => (this.cache = {}))); }, // We need this to be publically available for cases where we get the report history @@ -169,19 +169,15 @@ export default class ReportHistoryStore { return; } - const newCache = _.reduce( - newHistory.reverse(), - (prev, curr) => { - if (!_.findWhere(prev, {sequenceNumber: curr.sequenceNumber})) { - prev.unshift(curr); - } - return prev; - }, - this.cache[reportID] || [], - ); + const newCache = newHistory.reverse().reduce((prev, curr) => { + if (!prev.some((item) => item.sequenceNumber === curr.sequenceNumber)) { + prev.unshift(curr); + } + return prev; + }, this.cache[reportID] || []); // Sort items in case they have become out of sync - this.cache[reportID] = _.sortBy(newCache, 'sequenceNumber').reverse(); + this.cache[reportID] = newCache.sort((a, b) => b.sequenceNumber - a.sequenceNumber); } /** @@ -195,19 +191,15 @@ export default class ReportHistoryStore { return; } - const newCache = _.reduce( - newHistory.reverse(), - (prev, curr) => { - if (!_.findWhere(prev, {reportActionTimestamp: curr.reportActionTimestamp})) { - prev.unshift(curr); - } - return prev; - }, - this.cache[reportID] || [], - ); + const newCache = newHistory.reverse().reduce((prev, curr) => { + if (!prev.some((item) => item.reportActionTimestamp === curr.reportActionTimestamp)) { + prev.unshift(curr); + } + return prev; + }, this.cache[reportID] || []); // Sort items in case they have become out of sync - this.cache[reportID] = _.sortBy(newCache, 'reportActionTimestamp').reverse(); + this.cache[reportID] = newCache.sort((a, b) => b.reportActionTimestamp - a.reportActionTimestamp); } /** @@ -228,7 +220,7 @@ export default class ReportHistoryStore { // We'll poll the API for the un-cached history const cachedHistory = this.cache[reportID] || []; - const firstHistoryItem = _.first(cachedHistory) || {}; + const firstHistoryItem = cachedHistory[0] || {}; // Grab the most recent sequenceNumber we have and poll the API for fresh data this.API.Report_GetHistory({ @@ -290,7 +282,7 @@ export default class ReportHistoryStore { const cachedHistory = this.cache[reportID] || []; // If comment is not in cache then fetch it - if (_.isEmpty(cachedHistory)) { + if (cachedHistory.length === 0) { return this.getFlatHistory(reportID); } diff --git a/lib/Templates.jsx b/lib/Templates.jsx index e5e21fe8..17be9b2f 100644 --- a/lib/Templates.jsx +++ b/lib/Templates.jsx @@ -1,6 +1,6 @@ -/* eslint-disable max-classes-per-file */ -import _ from 'underscore'; import $ from 'jquery'; +import {template as createTemplate} from 'lodash'; +import * as Utils from './utils'; /** * JS Templating system, powered by underscore template @@ -37,7 +37,7 @@ export default (function () { */ get(data = {}) { if (!this.compiled) { - this.compiled = _.template(this.templateValue); + this.compiled = createTemplate(this.templateValue); this.templateValue = ''; } return this.compiled(data); @@ -71,7 +71,7 @@ export default (function () { // eslint-disable-next-line no-undef dataToCompile.nestedTemplate = Templates.get; if (!this.compiled) { - this.compiled = _.template($(`#${this.id}`).html()); + this.compiled = createTemplate($(`#${this.id}`).html()); } return this.compiled(dataToCompile); } @@ -85,7 +85,7 @@ export default (function () { */ function getTemplate(templatePath) { let template = templateStore; - _.each(templatePath, (pathname) => { + templatePath.forEach((pathname) => { template = template[pathname]; }); return template; @@ -104,7 +104,7 @@ export default (function () { for (let argumentNumber = 0; argumentNumber < wantedNamespace.length; argumentNumber++) { currentArgument = wantedNamespace[argumentNumber]; - if (_.isUndefined(namespace[currentArgument])) { + if (namespace[currentArgument] === undefined) { namespace[currentArgument] = {}; } namespace = namespace[currentArgument]; @@ -122,7 +122,7 @@ export default (function () { */ get(templatePath, data = {}) { const template = getTemplate(templatePath); - if (_.isUndefined(template)) { + if (template === undefined) { throw Error(`Template '${templatePath}' is not defined`); } @@ -141,7 +141,7 @@ export default (function () { * @return {Boolean} */ has(templatePath) { - return !_.isUndefined(getTemplate(templatePath)); + return getTemplate(templatePath) !== undefined; }, /** @@ -169,13 +169,13 @@ export default (function () { */ register(wantedNamespace, templateData) { const namespace = getTemplateNamespace(wantedNamespace); - _.each(_.keys(templateData), (key) => { + Object.keys(templateData).forEach((key) => { const template = templateData[key]; - if (_.isObject(template)) { + if (Utils.isObject(template)) { // If the template is an object, add templates for all keys namespace[key] = {}; - _.each(_.keys(template), (templateKey) => { + Object.keys(template).forEach((templateKey) => { namespace[key][templateKey] = new InlineTemplate(template[templateKey]); }); } else { diff --git a/lib/components/StepProgressBar.js b/lib/components/StepProgressBar.js index d682ef4a..f6564a88 100644 --- a/lib/components/StepProgressBar.js +++ b/lib/components/StepProgressBar.js @@ -1,5 +1,4 @@ import React from 'react'; -import _ from 'underscore'; import PropTypes from 'prop-types'; import cn from 'classnames'; import * as UIConstants from '../CONST'; @@ -24,7 +23,7 @@ const propTypes = { */ function StepProgressBar({steps, currentStep}) { const isCurrentStep = (step) => step.id === currentStep; - const currentStepIndex = Math.max(0, _.findIndex(steps, isCurrentStep)); + const currentStepIndex = Math.max(0, steps.findIndex(isCurrentStep)); const renderStep = (step, i) => { let status = currentStepIndex === i ? UIConstants.UI.ACTIVE : ''; @@ -52,7 +51,7 @@ function StepProgressBar({steps, currentStep}) { id="js_steps_progress" className="progress-wrapper" > -
    {_.map(steps, renderStep)}
    +
    {steps.map(renderStep)}
    ); } diff --git a/lib/components/form/element/combobox.js b/lib/components/form/element/combobox.js index 6843cd51..53a6e10b 100644 --- a/lib/components/form/element/combobox.js +++ b/lib/components/form/element/combobox.js @@ -2,12 +2,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import cn from 'classnames'; -import _ from 'underscore'; -import get from 'lodash/get'; -import has from 'lodash/has'; -import uniqBy from 'lodash/uniqBy'; +import {defer, has, isEqual, template, uniqBy} from 'lodash'; import Str from '../../../str'; import DropDown from './dropdown'; +import * as Utils from '../../../utils'; const propTypes = { // These are the elements to show in the dropdown @@ -54,6 +52,7 @@ const propTypes = { // that are already selected with the check mark. alreadySelectedOptions: PropTypes.arrayOf( PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), text: PropTypes.string, }), @@ -186,39 +185,39 @@ class Combobox extends React.Component { } // Our dropdown will be open, so we need to listen for our click away events // and put focus on the input - _.defer(this.resetClickAwayHandler); + defer(this.resetClickAwayHandler); $(this.value).focus().select(); } // eslint-disable-next-line react/no-unsafe UNSAFE_componentWillReceiveProps(nextProps) { - if (_.isUndefined(nextProps.value) || _.isEmpty(nextProps.value) || _.isEqual(nextProps.value, this.state.currentValue)) { + if (!nextProps.value || isEqual(nextProps.value, this.state.currentValue)) { return; } this.setValue(nextProps.value); - if (!_.isUndefined(nextProps.options)) { + if (nextProps.options !== undefined) { // If the options have an id property, we use that to compare them and determine if they changed, if not // we'll use the whole options array. if (has(nextProps.options, '0.id')) { if ( - !_.isEqual(_.pluck(nextProps.options, 'id'), _.pluck(this.props.options, 'id')) || - !_.isEqual(_.pluck(nextProps.alreadySelectedOptions, 'id'), _.pluck(this.props.alreadySelectedOptions, 'id')) + nextProps.options.some((option, index) => !isEqual(option.id, this.props.options[index].id)) || + nextProps.alreadySelectedOptions.some((alreadySelectedOption, index) => !isEqual(alreadySelectedOption.id, this.props.alreadySelectedOptions[index].id)) ) { this.reset(false, nextProps.options, nextProps.alreadySelectedOptions); } - } else if (!_.isEqual(nextProps.options, this.props.options) || !_.isEqual(nextProps.alreadySelectedOptions, this.props.alreadySelectedOptions)) { + } else if (!isEqual(nextProps.options, this.props.options) || !isEqual(nextProps.alreadySelectedOptions, this.props.alreadySelectedOptions)) { this.reset(false, nextProps.options, nextProps.alreadySelectedOptions); } } - if (!_.isUndefined(nextProps.openOnInit) && !_.isEqual(nextProps.openOnInit, this.props.openOnInit)) { + if (nextProps.openOnInit !== undefined && !isEqual(nextProps.openOnInit, this.props.openOnInit)) { this.setState({ isDropdownOpen: nextProps.openOnInit, }); } - if (!_.isUndefined(nextProps.isReadOnly) && !_.isEqual(nextProps.isReadOnly, this.props.isReadOnly)) { + if (nextProps.isReadOnly !== undefined && !isEqual(nextProps.isReadOnly, this.props.isReadOnly)) { this.setState({ isDisabled: nextProps.isReadOnly, }); @@ -249,8 +248,8 @@ class Combobox extends React.Component { // Select the new item, set our new indexes, close the dropdown // Unselect all other options - let newSelectedIndex = _(this.options).findIndex({value: selectedValue}); - let currentlySelectedOption = _(this.options).findWhere({value: selectedValue}); + let newSelectedIndex = this.options.findIndex({value: selectedValue}); + let currentlySelectedOption = this.options.findWhere({value: selectedValue}); // If allowAnyValue is true and currentValue is absent then set it manually to what the user has entered. if (newSelectedIndex === -1 && this.props.allowAnyValue && selectedValue) { @@ -278,9 +277,9 @@ class Combobox extends React.Component { selectedIndex: newSelectedIndex, focusedIndex: newSelectedIndex, currentValue: selectedValue, - currentText: get(currentlySelectedOption, 'text', ''), + currentText: (currentlySelectedOption && currentlySelectedOption.text) || '', isDropdownOpen: false, - hasError: get(currentlySelectedOption, 'hasError', false), + hasError: (currentlySelectedOption && currentlySelectedOption.hasError) || false, }, stateUpdateCallback, ); @@ -448,7 +447,7 @@ class Combobox extends React.Component { const matchingOptionWithoutSMSDomain = (o) => (Str.isString(o) ? Str.removeSMSDomain(o.value) : o.value) === currentValue && !o.isFake; // We use removeSMSDomain here in case currentValue is a phone number - let defaultSelectedOption = _(this.options).find(matchingOptionWithoutSMSDomain); + let defaultSelectedOption = this.options.find(matchingOptionWithoutSMSDomain); // If no default was found and initialText was present then we can use initialText values if (!defaultSelectedOption && this.initialText) { @@ -480,13 +479,13 @@ class Combobox extends React.Component { // Get the divider index if we have one const findDivider = (option) => option.divider; - const dividerIndex = _.findIndex(this.options, findDivider); + const dividerIndex = this.options.findIndex(findDivider); // Split into two arrays everything before and after the divider (if the divider does not exist then we'll return a single array) const splitOptions = dividerIndex ? [this.options.slice(0, dividerIndex + 1), this.options.slice(dividerIndex + 1)] : [this.options]; const formatOption = (option) => ({ focused: false, - isSelected: option.selected && (_.isEqual(option.value, currentValue) || Boolean(_.findWhere(alreadySelected, {value: option.value}))), + isSelected: option.selected && (isEqual(option.value, currentValue) || Boolean(alreadySelected.find((item) => item.value === option.value))), ...option, }); @@ -500,9 +499,13 @@ class Combobox extends React.Component { }; // Take each array and format it, sort it, and move selected items to top (if applicable) - const formatOptions = (array) => _.chain(array).map(formatOption).sortBy(sortByOption).first(this.props.maxItemsToShow).value(); + const formatOptions = (array) => + array + .map(formatOption) + .sort((a, b) => sortByOption(a) - sortByOption(b)) + .slice(0, this.props.maxItemsToShow); - const truncatedOptions = _.chain(splitOptions).map(formatOptions).flatten().value(); + const truncatedOptions = splitOptions.map(formatOptions).flat(); if (!truncatedOptions.length) { truncatedOptions.push({ @@ -542,18 +545,18 @@ class Combobox extends React.Component { setValue(val) { // We need to look in `this.options` for the matching option because `this.state.options` is a truncated list // and might not have every option - const optionMatchingVal = _.findWhere(this.options, {value: val}); - const currentText = get(optionMatchingVal, 'text', ''); + const optionMatchingVal = this.options.find((option) => option.value === val); + const currentText = optionMatchingVal?.text || ''; const deselectOption = (initialOption) => { const option = initialOption; - const isSelected = _.isEqual(option.value, val); - option.isSelected = isSelected || Boolean(_.findWhere(this.props.alreadySelectedOptions, {value: option.value})); + const isSelected = isEqual(option.value, val); + option.isSelected = isSelected || Boolean(this.props.alreadySelectedOptions.find((optionItem) => optionItem.value === option.value)); return option; }; - const deselectOptions = (options) => _(options).map(deselectOption); + const deselectOptions = (options) => options.map(deselectOption); const setValueState = (state) => ({ currentValue: val, @@ -575,8 +578,8 @@ class Combobox extends React.Component { // See if there is a value in the options that matches the text we want to set. If the option // does exist, then use the text property of that option for the text to display. If the option // does not exist, then just display whatever value was passed - const optionMatchingVal = _.findWhere(this.options, {value: val}); - const currentText = get(optionMatchingVal, 'text', val); + const optionMatchingVal = this.options.find((option) => option.value === val); + const currentText = (optionMatchingVal && optionMatchingVal.text) || val; this.initialValue = currentText; this.initialText = currentText; this.setState({currentText}); @@ -668,7 +671,7 @@ class Combobox extends React.Component { }; const setValueState = (state) => ({ - options: _(state.options).map(resetFocusedProperty), + options: state.options.map(resetFocusedProperty), }); this.setState(setValueState); @@ -855,11 +858,13 @@ class Combobox extends React.Component { const formatOption = (option) => ({ focused: false, - isSelected: _.isEqual(option.value ? option.value.toUpperCase : '', value.toUpperCase()) || Boolean(_.findWhere(this.props.alreadySelectedOptions, {value: option.value})), + isSelected: + isEqual(option.value ? option.value.toUpperCase : '', value.toUpperCase()) || + Boolean(this.props.alreadySelectedOptions.find((optionItem) => optionItem.value === option.value)), ...option, }); - const options = _(matches).map(formatOption); + const options = matches.map(formatOption); // Focus the first option if there is one and show a message dependent on what options are present if (options.length) { @@ -876,7 +881,7 @@ class Combobox extends React.Component { } } else { options.push({ - text: this.props.allowAnyValue ? value : _.template(this.props.noResultsText)({value}), + text: this.props.allowAnyValue ? value : template(this.props.noResultsText)({value}), value: this.props.allowAnyValue ? value : '', isSelectable: this.props.allowAnyValue, isFake: true, @@ -933,7 +938,7 @@ class Combobox extends React.Component { aria-label="..." onChange={this.performSearch} onKeyDown={this.closeDropdownOnTabOut} - value={this.props.propertyToDisplay === 'value' ? _.unescape(this.state.currentValue) : _.unescape(this.state.currentText.replace(/ /g, ''))} + value={this.props.propertyToDisplay === 'value' ? Utils.unescape(this.state.currentValue) : Utils.unescape(this.state.currentText.replace(/ /g, ''))} onFocus={this.openDropdown} autoComplete="off" placeholder={this.props.placeholder} diff --git a/lib/components/form/element/dropdown.js b/lib/components/form/element/dropdown.js index df78770e..2b1d7d45 100644 --- a/lib/components/form/element/dropdown.js +++ b/lib/components/form/element/dropdown.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import cn from 'classnames'; -import _ from 'underscore'; +import {uniqueId} from 'lodash'; import DropDownItem from './dropdownItem'; const propTypes = { @@ -65,7 +65,7 @@ class DropDown extends React.Component { renderOption(option) { return ( {_.map(options, this.renderOption)}; + return
      {options.map(this.renderOption)}
    ; } } diff --git a/lib/components/form/element/switch.js b/lib/components/form/element/switch.js index 8ad6807b..96447352 100644 --- a/lib/components/form/element/switch.js +++ b/lib/components/form/element/switch.js @@ -3,7 +3,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import cn from 'classnames'; -import _ from 'underscore'; /** * Form Element Switch - Displays an on/off switch @@ -95,18 +94,14 @@ class Switch extends React.Component { } e.preventDefault(); - Modal.confirm( - _.extend( - { - onYesCallback: () => { - // Toggle the checked property and then fire our change handler - this.checkbox.checked = !this.getValue(); - Func.invoke(this.props.onChange, [this.getValue()]); - }, - }, - this.props.confirm, - ), - ); + Modal.confirm({ + ...(this.props.confirm ?? {}), + onYesCallback: () => { + // Toggle the checked property and then fire our change handler + this.checkbox.checked = !this.getValue(); + Func.invoke(this.props.onChange, [this.getValue()]); + }, + }); return false; } diff --git a/lib/mixins/PubSub.jsx b/lib/mixins/PubSub.jsx index 8be94395..b5552759 100644 --- a/lib/mixins/PubSub.jsx +++ b/lib/mixins/PubSub.jsx @@ -1,8 +1,7 @@ -import _ from 'underscore'; import PubSubModule from '../PubSub'; -import {isWindowAvailable} from '../utils'; +import * as Utils from '../utils'; -const PubSub = (isWindowAvailable() && window.PubSub) || PubSubModule; +const PubSub = (Utils.isWindowAvailable() && window.PubSub) || PubSubModule; /** * This mixin sets up automatic PubSub bindings which will be removed when @@ -18,7 +17,7 @@ const PubSub = (isWindowAvailable() && window.PubSub) || PubSubModule; * } * }); */ -export default { +const PubSubMixin = { UNSAFE_componentWillMount() { this.eventIds = []; }, @@ -45,6 +44,10 @@ export default { * When the component is unmounted, we want to subscribe from all of our event IDs */ componentWillUnmount() { - _.each(this.eventIds, _.bind(PubSub.unsubscribe, PubSub)); + this.eventIds.forEach((eventId) => { + PubSub.unsubscribe(eventId); + }); }, }; + +export default PubSubMixin; diff --git a/lib/mixins/extraClasses.js b/lib/mixins/extraClasses.js index 13acbfb1..a0cf9b9e 100644 --- a/lib/mixins/extraClasses.js +++ b/lib/mixins/extraClasses.js @@ -23,11 +23,11 @@ * } */ -import {isWindowAvailable} from '../utils'; +import * as Utils from '../utils'; export default { propTypes: { - extraClasses: isWindowAvailable() && window.PropTypes.oneOfType([window.PropTypes.string, window.PropTypes.array, window.PropTypes.object]), + extraClasses: Utils.isWindowAvailable() && window.PropTypes.oneOfType([window.PropTypes.string, window.PropTypes.array, window.PropTypes.object]), }, UNSAFE_componentWillReceiveProps(nextProps) { diff --git a/lib/str.ts b/lib/str.ts index 0e9da4a4..210a72f5 100644 --- a/lib/str.ts +++ b/lib/str.ts @@ -1,10 +1,9 @@ /* eslint-disable no-control-regex */ -import lodashEscape from 'lodash/escape'; -import lodashUnescape from 'lodash/unescape'; import {parsePhoneNumber} from 'awesome-phonenumber'; import * as HtmlEntities from 'html-entities'; import * as Constants from './CONST'; import * as UrlPatterns from './Url'; +import * as Utils from './utils'; const REMOVE_SMS_DOMAIN_PATTERN = /@expensify\.sms/gi; @@ -87,11 +86,7 @@ const Str = { * @param s The string to decode. * @returns The decoded string. */ - htmlDecode(s: string): string { - // Use jQuery if it exists or else use html-entities - if (typeof jQuery !== 'undefined') { - return jQuery('