Reassessment of client helpers (#19249)

pull/19276/head
Tasso Evangelista 5 years ago committed by GitHub
parent b7e7fc75eb
commit b1e2d64e13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      app/autolinker/client/client.js
  2. 3
      app/emoji-custom/client/lib/emojiCustom.js
  3. 6
      app/emoji/client/emojiPicker.js
  4. 1
      app/theme/client/index.js
  5. 12
      app/ui-flextab/client/tabs/uploadedFilesList.js
  6. 5
      app/ui-message/client/popup/messagePopupConfig.js
  7. 3
      app/ui-sidenav/client/toolbar.js
  8. 2
      client/account/AccountProfileForm.js
  9. 2
      client/account/AccountProfilePage.js
  10. 2
      client/account/sidebarItems.js
  11. 2
      client/admin/apps/AppSettings.js
  12. 4
      client/admin/info/InformationRoute.js
  13. 2
      client/admin/routes.js
  14. 2
      client/admin/sidebarItems.js
  15. 2
      client/admin/users/UsersTable.js
  16. 2
      client/components/CustomFieldsForm.js
  17. 2
      client/components/basic/userStatus/UserStatus.js
  18. 4
      client/helpers/capitalize.js
  19. 26
      client/helpers/createSidebarItems.js
  20. 15
      client/helpers/download.js
  21. 9
      client/helpers/getDateRange.js
  22. 1
      client/helpers/getUserEmailAddress.js
  23. 2
      client/hooks/useForm.ts
  24. 43
      client/lib/capitalize.spec.ts
  25. 7
      client/lib/capitalize.ts
  26. 14
      client/lib/createRouteGroup.ts
  27. 41
      client/lib/createSidebarItems.ts
  28. 90
      client/lib/download.spec.ts
  29. 43
      client/lib/download.ts
  30. 80
      client/lib/escapeRegExp.spec.ts
  31. 7
      client/lib/escapeRegExp.ts
  32. 15
      client/lib/getDateRange.ts
  33. 4
      client/lib/getUserEmailAddress.ts
  34. 9
      client/lib/saveFile.js
  35. 0
      client/lib/statusColors.ts
  36. 1
      client/lib/toastr.js
  37. 41
      client/lib/userData.js
  38. 71
      client/lib/userData.ts
  39. 1
      client/main.js
  40. 2
      client/omnichannel/agents/AgentEdit.js
  41. 2
      client/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js
  42. 2
      client/omnichannel/routes.js
  43. 2
      client/omnichannel/sidebarItems.js
  44. 4
      client/startup/startup.js
  45. 14
      ee/app/engagement-dashboard/client/components/ChannelsTab/TableSection.js
  46. 8
      ee/app/engagement-dashboard/client/components/MessagesTab/MessagesPerChannelSection.js
  47. 8
      ee/app/engagement-dashboard/client/components/MessagesTab/MessagesSentSection.js
  48. 10
      ee/app/engagement-dashboard/client/components/UsersTab/ActiveUsersSection.js
  49. 8
      ee/app/engagement-dashboard/client/components/UsersTab/NewUsersSection.js
  50. 20
      ee/app/engagement-dashboard/client/components/UsersTab/UsersByTimeOfTheDaySection.js
  51. 1
      mocha.opts.js
  52. 463
      package-lock.json
  53. 2
      package.json

@ -7,6 +7,7 @@ import Autolinker from 'autolinker';
import { settings } from '../../settings';
import { callbacks } from '../../callbacks';
import { escapeRegExp } from '../../../client/lib/escapeRegExp';
let config;
@ -33,7 +34,7 @@ const renderMessage = (message) => {
let msgParts;
let regexTokens;
if (message.tokens && message.tokens.length) {
regexTokens = new RegExp(`(${ (message.tokens || []).map(({ token }) => RegExp.escape(token)) })`, 'g');
regexTokens = new RegExp(`(${ (message.tokens || []).map(({ token }) => escapeRegExp(token)) })`, 'g');
msgParts = message.html.split(regexTokens);
} else {
msgParts = [message.html];

@ -7,6 +7,7 @@ import { RoomManager } from '../../../ui-utils/client';
import { emoji, EmojiPicker } from '../../../emoji/client';
import { CachedCollectionManager } from '../../../ui-cached-collection/client';
import { APIClient } from '../../../utils/client';
import { escapeRegExp } from '../../../../client/lib/escapeRegExp';
export const getEmojiUrlFromName = function(name, extension) {
Session.get;
@ -126,7 +127,7 @@ export const updateEmojiCustom = function(emojiData) {
};
const customRender = (html) => {
const emojisMatchGroup = emoji.packages.emojiCustom.list.map(RegExp.escape).join('|');
const emojisMatchGroup = emoji.packages.emojiCustom.list.map(escapeRegExp).join('|');
if (emojisMatchGroup !== emoji.packages.emojiCustom._regexpSignature) {
emoji.packages.emojiCustom._regexpSignature = emojisMatchGroup;
emoji.packages.emojiCustom._regexp = new RegExp(`<object[^>]*>.*?<\/object>|<span[^>]*>.*?<\/span>|<(?:object|embed|svg|img|div|span|p|a)[^>]*>|(${ emojisMatchGroup })`, 'gi');

@ -4,12 +4,12 @@ import { ReactiveDict } from 'meteor/reactive-dict';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import { escapeRegExp } from '../../../client/lib/escapeRegExp';
import '../../theme/client/imports/components/emojiPicker.css';
import { t } from '../../utils/client';
import { EmojiPicker } from './lib/EmojiPicker';
import { emoji } from '../lib/rocketchat';
import './emojiPicker.html';
import '../../theme/client/imports/components/emojiPicker.css';
const ESCAPE = 27;
@ -50,7 +50,7 @@ function getEmojisBySearchTerm(searchTerm) {
EmojiPicker.currentCategory.set('');
const searchRegExp = new RegExp(RegExp.escape(searchTerm.replace(/:/g, '')), 'i');
const searchRegExp = new RegExp(escapeRegExp(searchTerm.replace(/:/g, '')), 'i');
for (let current in emoji.list) {
if (!emoji.list.hasOwnProperty(current)) {

@ -1,3 +1,4 @@
import 'toastr/build/toastr.min.css';
import './main.css';
import './vendor/photoswipe.css';
import './vendor/fontello/css/fontello.css';

@ -10,6 +10,7 @@ import { canDeleteMessage, getURL, handleError, t, APIClient } from '../../../ut
import { popover, modal } from '../../../ui-utils/client';
import { Rooms, Messages } from '../../../models/client';
import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager';
import { download } from '../../../../client/lib/download';
const LIST_SIZE = 50;
const DEBOUNCE_TIME_TO_SEARCH_IN_MS = 500;
@ -255,13 +256,10 @@ Template.uploadedFilesList.events({
icon: 'download',
name: t('Download'),
action: () => {
const a = document.createElement('a');
a.href = getURL(this.file.url);
a.download = this.file.name;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(this.file.url);
a.remove();
const URL = window.webkitURL ?? window.URL;
const href = getURL(this.file.url);
download(href, this.file.name);
URL.revokeObjectURL(this.file.url);
},
},
],

@ -17,6 +17,7 @@ import { customMessagePopups } from './customMessagePopups';
import './messagePopupConfig.html';
import './messagePopupSlashCommand.html';
import './messagePopupUser.html';
import { escapeRegExp } from '../../../../client/lib/escapeRegExp';
const reloadUsersFromRoomMessages = (rid, template) => {
const user = Meteor.userId() && Meteor.users.findOne(Meteor.userId(), { fields: { username: 1 } });
@ -144,7 +145,7 @@ const getEmojis = (collection, filter) => {
return [];
}
const regExp = new RegExp(RegExp.escape(filter), 'i');
const regExp = new RegExp(escapeRegExp(filter), 'i');
const recents = EmojiPicker.getRecent().map((item) => `:${ item }:`);
return Object.keys(collection)
@ -208,7 +209,7 @@ Template.messagePopupConfig.helpers({
getFilter: (collection, filter = '', cb) => {
const { rid } = this;
const filterText = filter.trim();
const filterRegex = filterText !== '' && new RegExp(`${ RegExp.escape(filterText) }`, 'i');
const filterRegex = filterText !== '' && new RegExp(`${ escapeRegExp(filterText) }`, 'i');
const items = template.usersFromRoomMessages
.find(

@ -12,6 +12,7 @@ import { Rooms, Subscriptions } from '../../models';
import { roomTypes } from '../../utils';
import { hasAtLeastOnePermission } from '../../authorization';
import { menu } from '../../ui-utils';
import { escapeRegExp } from '../../../client/lib/escapeRegExp';
let filterText = '';
let usernamesFromClient;
@ -130,7 +131,7 @@ Template.toolbar.helpers({
query.t = 'd';
}
const searchQuery = new RegExp(RegExp.escape(filterText), 'i');
const searchQuery = new RegExp(escapeRegExp(filterText), 'i');
query.$or = [
{ name: searchQuery },
{ fname: searchQuery },

@ -6,7 +6,7 @@ import { useTranslation } from '../contexts/TranslationContext';
import { isEmail } from '../../app/utils/lib/isEmail.js';
import { useToastMessageDispatch } from '../contexts/ToastMessagesContext';
import { useMethod } from '../contexts/ServerContext';
import { getUserEmailAddress } from '../helpers/getUserEmailAddress';
import { getUserEmailAddress } from '../lib/getUserEmailAddress';
import { UserAvatarEditor } from '../components/basic/avatar/UserAvatarEditor';
import CustomFieldsForm from '../components/CustomFieldsForm';
import UserStatusMenu from '../components/basic/userStatus/UserStatusMenu';

@ -13,7 +13,7 @@ import { useToastMessageDispatch } from '../contexts/ToastMessagesContext';
import { useMethod } from '../contexts/ServerContext';
import { useSetModal } from '../contexts/ModalContext';
import { useUpdateAvatar } from '../hooks/useUpdateAvatar';
import { getUserEmailAddress } from '../helpers/getUserEmailAddress';
import { getUserEmailAddress } from '../lib/getUserEmailAddress';
import ActionConfirmModal from './ActionConfirmModal';
const getInitialValues = (user) => ({

@ -3,7 +3,7 @@ import { HTML } from 'meteor/htmljs';
import { hasPermission } from '../../app/authorization/client';
import { createTemplateForComponent } from '../reactAdapters';
import { settings } from '../../app/settings';
import { createSidebarItems } from '../helpers/createSidebarItems';
import { createSidebarItems } from '../lib/createSidebarItems';
createTemplateForComponent('accountFlex', () => import('./AccountSidebar'), {
renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }), // eslint-disable-line new-cap

@ -3,7 +3,7 @@ import { Box } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { MemoizedSetting } from '../settings/Setting';
import { capitalize } from '../../helpers/capitalize';
import { capitalize } from '../../lib/capitalize';
import { useRouteParameter } from '../../contexts/RouterContext';
import MarkdownText from '../../components/basic/MarkdownText';

@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react';
import { usePermission } from '../../contexts/AuthorizationContext';
import NotAuthorizedPage from '../../components/NotAuthorizedPage';
import { useMethod, useServerInformation, useEndpoint } from '../../contexts/ServerContext';
import { downloadJsonAsAFile } from '../../helpers/download';
import { downloadJsonAs } from '../../lib/download';
import InformationPage from './InformationPage';
const InformationRoute = React.memo(function InformationRoute() {
@ -61,7 +61,7 @@ const InformationRoute = React.memo(function InformationRoute() {
if (isLoading) {
return;
}
downloadJsonAsAFile(statistics, 'statistics');
downloadJsonAs(statistics, 'statistics');
};
if (canViewStatistics) {

@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { createRouteGroup } from '../helpers/createRouteGroup';
import { createRouteGroup } from '../lib/createRouteGroup';
export const registerAdminRoute = createRouteGroup('admin', '/admin', () => import('./AdministrationRouter'));

@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor';
import { hasPermission, hasRole } from '../../app/authorization/client';
import { createTemplateForComponent } from '../reactAdapters';
import { createSidebarItems } from '../helpers/createSidebarItems';
import { createSidebarItems } from '../lib/createSidebarItems';
createTemplateForComponent('adminFlex', () => import('./sidebar/AdminSidebar'), {
renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }), // eslint-disable-line new-cap

@ -4,7 +4,7 @@ import React, { useMemo, useCallback, useState } from 'react';
import UserAvatar from '../../components/basic/avatar/UserAvatar';
import GenericTable from '../../components/GenericTable';
import { capitalize } from '../../helpers/capitalize';
import { capitalize } from '../../lib/capitalize';
import { useTranslation } from '../../contexts/TranslationContext';
import { useRoute } from '../../contexts/RouterContext';
import { useEndpointData } from '../../hooks/useEndpointData';

@ -4,7 +4,7 @@ import { TextInput, Select, Field } from '@rocket.chat/fuselage';
import { useSetting } from '../contexts/SettingsContext';
import { useForm } from '../hooks/useForm';
import { useTranslation } from '../contexts/TranslationContext';
import { capitalize } from '../helpers/capitalize';
import { capitalize } from '../lib/capitalize';
const CustomTextInput = ({ name, required, minLength, maxLength, setState, state, className }) => {
const t = useTranslation();

@ -1,7 +1,7 @@
import React from 'react';
import { Box } from '@rocket.chat/fuselage';
import statusColors from '../../../helpers/statusColors';
import statusColors from '../../../lib/statusColors';
const UserStatus = React.memo(({ status, ...props }) => <Box size='x12' borderRadius='full' backgroundColor={statusColors[status]} {...props}/>);

@ -1,4 +0,0 @@
export const capitalize = (s) => {
if (typeof s !== 'string') { return ''; }
return s.charAt(0).toUpperCase() + s.slice(1);
};

@ -1,26 +0,0 @@
export const createSidebarItems = (initialItems = []) => {
const items = initialItems;
let updateCb = () => {};
const itemsSubscription = {
subscribe: (cb) => {
updateCb = cb;
return () => {
updateCb = () => {};
};
},
getCurrentValue: () => items,
};
const registerSidebarItem = (item) => {
items.push(item);
updateCb();
};
const unregisterSidebarItem = (label) => {
const index = items.findIndex(({ i18nLabel }) => i18nLabel === label);
delete items[index];
updateCb();
};
return { registerSidebarItem, unregisterSidebarItem, itemsSubscription };
};

@ -1,15 +0,0 @@
export const downloadJsonAsAFile = (jsonData, name = 'jsonfile') => {
const filename = `${ name }.json`;
const contentType = 'application/json;charset=utf-8;';
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
const blob = new Blob([decodeURIComponent(encodeURI(JSON.stringify(jsonData)))], { type: contentType });
return navigator.msSaveOrOpenBlob(blob, filename);
}
const aElement = document.createElement('a');
aElement.download = filename;
aElement.href = `data:${ contentType },${ encodeURIComponent(JSON.stringify(jsonData)) }`;
aElement.target = '_blank';
document.body.appendChild(aElement);
aElement.click();
document.body.removeChild(aElement);
};

@ -1,9 +0,0 @@
import moment from 'moment';
export const getDateRange = () => {
const today = moment(new Date());
return {
start: `${ moment(new Date(today.year(), today.month(), today.date(), 0, 0, 0)).utc().format('YYYY-MM-DDTHH:mm:ss') }Z`,
end: `${ moment(new Date(today.year(), today.month(), today.date(), 23, 59, 59)).utc().format('YYYY-MM-DDTHH:mm:ss') }Z`,
};
};

@ -1 +0,0 @@
export const getUserEmailAddress = (user) => user.emails?.find(({ address }) => !!address)?.address;

@ -1,6 +1,6 @@
import { useCallback, useReducer, useMemo, ChangeEvent } from 'react';
import { capitalize } from '../helpers/capitalize';
import { capitalize } from '../lib/capitalize';
type Field = {
name: string;

@ -0,0 +1,43 @@
import assert from 'assert';
import { describe, it } from 'mocha';
import { capitalize } from './capitalize';
describe('capitalize', () => {
it('should convert "xyz" to "Xyz"', () => {
assert.equal(capitalize('xyz'), 'Xyz');
});
it('should convert "xyz xyz" to "Xyz xyz"', () => {
assert.equal(capitalize('xyz xyz'), 'Xyz xyz');
});
it('should convert " xyz" to " xyz"', () => {
assert.equal(capitalize(' xyz'), ' xyz');
});
it('should convert undefined to ""', () => {
assert.equal(capitalize(undefined as unknown as string), '');
});
it('should convert null to ""', () => {
assert.equal(capitalize(null as unknown as string), '');
});
it('should convert false to ""', () => {
assert.equal(capitalize(false as unknown as string), '');
});
it('should convert true to ""', () => {
assert.equal(capitalize(true as unknown as string), '');
});
it('should convert 0 to ""', () => {
assert.equal(capitalize(0 as unknown as string), '');
});
it('should convert 1 to ""', () => {
assert.equal(capitalize(1 as unknown as string), '');
});
});

@ -0,0 +1,7 @@
export const capitalize = (s: string): string => {
if (typeof s !== 'string') {
return '';
}
return s.charAt(0).toUpperCase() + s.slice(1);
};

@ -1,14 +1,24 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import type { ElementType } from 'react';
import { renderRouteComponent } from '../reactAdapters';
export const createRouteGroup = (name, prefix, importRouter) => {
type RouteRegister = {
(path: string, params: {
name: string;
lazyRouteComponent: () => Promise<ElementType>;
props: Record<string, unknown>;
action: (params?: Record<string, string>, queryParams?: Record<string, string>) => void;
}): void;
};
export const createRouteGroup = (name: string, prefix: string, importRouter: () => Promise<ElementType>): RouteRegister => {
const routeGroup = FlowRouter.group({
name,
prefix,
});
const registerRoute = (path, { lazyRouteComponent, props, action, ...options } = {}) => {
const registerRoute: RouteRegister = (path, { lazyRouteComponent, props, action, ...options }) => {
routeGroup.route(path, {
...options,
action: (params, queryParams) => {

@ -0,0 +1,41 @@
import type { Subscription } from 'use-subscription';
type SidebarItem = {
i18nLabel: string;
};
export const createSidebarItems = (initialItems: SidebarItem[] = []): ({
registerSidebarItem: (item: SidebarItem) => void;
unregisterSidebarItem: (i18nLabel: SidebarItem['i18nLabel']) => void;
itemsSubscription: Subscription<SidebarItem[]>;
}) => {
const items = initialItems;
let updateCb: (() => void) = () => undefined;
const itemsSubscription: Subscription<SidebarItem[]> = {
subscribe: (cb) => {
updateCb = cb;
return (): void => {
updateCb = (): void => undefined;
};
},
getCurrentValue: () => items,
};
const registerSidebarItem = (item: SidebarItem): void => {
items.push(item);
updateCb();
};
const unregisterSidebarItem = (i18nLabel: SidebarItem['i18nLabel']): void => {
const index = items.findIndex((item) => item.i18nLabel === i18nLabel);
delete items[index];
updateCb();
};
return {
registerSidebarItem,
unregisterSidebarItem,
itemsSubscription,
};
};

@ -0,0 +1,90 @@
import 'jsdom-global/register';
import chai from 'chai';
import chaiSpies from 'chai-spies';
import { after, before, describe, it } from 'mocha';
import { download, downloadAs, downloadCsvAs, downloadJsonAs } from './download';
chai.use(chaiSpies);
const withURL = (): void => {
let createObjectURL: typeof URL.createObjectURL;
let revokeObjectURL: typeof URL.revokeObjectURL;
before(() => {
const blobs = new Map<string, Blob>();
createObjectURL = window.URL.createObjectURL;
revokeObjectURL = window.URL.revokeObjectURL;
window.URL.createObjectURL = (blob: Blob): string => {
const uuid = Math.random().toString(36).slice(2);
const url = `blob://${ uuid }`;
blobs.set(url, blob);
return url;
};
window.URL.revokeObjectURL = (url: string): void => {
blobs.delete(url);
};
});
after(() => {
window.URL.createObjectURL = createObjectURL;
window.URL.revokeObjectURL = revokeObjectURL;
});
};
describe('download', () => {
it('should work', () => {
const listener = chai.spy();
document.addEventListener('click', listener, false);
download('about:blank', 'blank');
document.removeEventListener('click', listener, false);
chai.expect(listener).to.have.been.called();
});
});
describe('downloadAs', () => {
withURL();
it('should work', () => {
const listener = chai.spy();
document.addEventListener('click', listener, false);
downloadAs({ data: [] }, 'blank');
document.removeEventListener('click', listener, false);
chai.expect(listener).to.have.been.called();
});
});
describe('downloadJsonAs', () => {
withURL();
it('should work', () => {
const listener = chai.spy();
document.addEventListener('click', listener, false);
downloadJsonAs({}, 'blank');
document.removeEventListener('click', listener, false);
chai.expect(listener).to.have.been.called();
});
});
describe('downloadCsvAs', () => {
withURL();
it('should work', () => {
const listener = chai.spy();
document.addEventListener('click', listener, false);
downloadCsvAs([[1, 2, 3], [4, 5, 6]], 'blank');
document.removeEventListener('click', listener, false);
chai.expect(listener).to.have.been.called();
});
});

@ -0,0 +1,43 @@
export const download = (href: string, filename: string): void => {
const anchorElement = document.createElement('a');
anchorElement.download = filename;
anchorElement.href = href;
anchorElement.target = '_blank';
document.body.appendChild(anchorElement);
anchorElement.click();
document.body.removeChild(anchorElement);
};
export const downloadAs = ({ data, ...options }: { data: BlobPart[] } & BlobPropertyBag, filename: string): void => {
const blob = new Blob(data, options);
if (navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(blob);
return;
}
const URL = window.webkitURL ?? window.URL;
const blobUrl = URL.createObjectURL(blob);
download(blobUrl, filename);
URL.revokeObjectURL(blobUrl);
};
export const downloadJsonAs = (jsonObject: unknown, basename: string): void => {
downloadAs({
data: [decodeURIComponent(encodeURI(JSON.stringify(jsonObject, null, 2)))],
type: 'application/json;charset=utf-8',
}, `${ basename }.json`);
};
export const downloadCsvAs = (csvData: unknown[][], basename: string): void => {
const escapeCell = (cell: unknown): string => `"${ String(cell).replace(/"/g, '""') }"`;
const content = csvData.reduce((content, row) => `${ content + row.map(escapeCell).join(';') }\n`, '');
downloadAs({
data: [decodeURIComponent(encodeURI(content))],
type: 'text/csv;charset=utf-8',
endings: 'native',
}, `${ basename }.csv`);
};

@ -0,0 +1,80 @@
import assert from 'assert';
import { describe, it } from 'mocha';
import { escapeRegExp } from './escapeRegExp';
describe('escapeRegExp', () => {
it('should keep strings with letters only unchanged', () => {
assert.equal(escapeRegExp('word'), 'word');
});
it('should escape slashes', () => {
assert.equal(escapeRegExp('/slashes/'), '\\/slashes\\/');
assert.equal(escapeRegExp('\\backslashes\\'), '\\\\backslashes\\\\');
assert.equal(escapeRegExp('\\border of word'), '\\\\border of word');
});
it('should escape special group', () => {
assert.equal(escapeRegExp('(?:non-capturing)'), '\\(\\?:non\\-capturing\\)');
assert.equal(
new RegExp(`${ escapeRegExp('(?:') }([^)]+)`).exec('(?:non-capturing)')?.[1],
'non-capturing',
);
assert.equal(escapeRegExp('(?=positive-lookahead)'), '\\(\\?=positive\\-lookahead\\)');
assert.equal(
new RegExp(`${ escapeRegExp('(?=') }([^)]+)`).exec('(?=positive-lookahead)')?.[1],
'positive-lookahead',
);
assert.equal(escapeRegExp('(?<=positive-lookbehind)'), '\\(\\?<=positive\\-lookbehind\\)');
assert.equal(
new RegExp(`${ escapeRegExp('(?<=') }([^)]+)`).exec('(?<=positive-lookbehind)')?.[1],
'positive-lookbehind',
);
assert.equal(escapeRegExp('(?!negative-lookahead)'), '\\(\\?!negative\\-lookahead\\)');
assert.equal(
new RegExp(`${ escapeRegExp('(?!') }([^)]+)`).exec('(?!negative-lookahead)')?.[1],
'negative-lookahead',
);
assert.equal(escapeRegExp('(?<!negative-lookbehind)'), '\\(\\?<!negative\\-lookbehind\\)');
assert.equal(
new RegExp(`${ escapeRegExp('(?<!') }([^)]+)`).exec('(?<!negative-lookbehind)')?.[1],
'negative-lookbehind',
);
assert.equal(escapeRegExp('[\\w]+'), '\\[\\\\w\\]\\+');
assert.equal(new RegExp(`${ escapeRegExp('[') }([^\\]]+)`).exec('[character class]')?.[1], 'character class');
assert.equal(new RegExp(escapeRegExp('<div>')).exec('<td><div></td>')?.[0], '<div>');
assert.equal(escapeRegExp('{5,2}'), '\\{5,2\\}');
assert.equal(
escapeRegExp('/([.*+?^=!:${}()|[\\]\\/\\\\])/g'),
'\\/\\(\\[\\.\\*\\+\\?\\^=!:\\$\\{\\}\\(\\)\\|\\[\\\\\\]\\\\\\/\\\\\\\\\\]\\)\\/g',
);
});
it('should not escape whitespace', () => {
assert.equal(escapeRegExp('\\n\\r\\t'), '\\\\n\\\\r\\\\t');
assert.equal(escapeRegExp('\n\r\t'), '\n\r\t');
});
it('throws an error for non-string argument', () => {
// @ts-ignore
assert.throws(() => escapeRegExp(false));
// @ts-ignore
assert.throws(() => escapeRegExp());
// @ts-ignore
assert.throws(() => escapeRegExp(null));
// @ts-ignore
assert.throws(() => escapeRegExp(42));
});
});

@ -0,0 +1,7 @@
export const escapeRegExp = (input: string): string => {
if (typeof input !== 'string') {
throw new TypeError('string expected');
}
return input.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
};

@ -0,0 +1,15 @@
import moment from 'moment';
export const getDateRange = (): {
start: string;
end: string;
} => {
const today = moment(new Date());
const start = moment(new Date(today.year(), today.month(), today.date(), 0, 0, 0));
const end = moment(new Date(today.year(), today.month(), today.date(), 23, 59, 59));
return {
start: start.toISOString(),
end: end.toISOString(),
};
};

@ -0,0 +1,4 @@
import type { IUser } from '../../definition/IUser';
export const getUserEmailAddress = (user: IUser): string | undefined =>
user.emails?.find(({ address }) => !!address)?.address;

@ -1,9 +0,0 @@
export const saveFile = (content, name = 'download') => {
const blob = new Blob([content], { type: 'text/plain' });
const anchor = document.createElement('a');
anchor.download = name;
anchor.href = (window.webkitURL || window.URL).createObjectURL(blob);
anchor.dataset.downloadurl = ['text/plain', anchor.download, anchor.href].join(':');
anchor.click();
};

@ -1 +0,0 @@
import 'toastr/build/toastr.min.css';

@ -1,41 +0,0 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Meteor } from 'meteor/meteor';
import { APIClient } from '../../app/utils/client';
import { Users } from '../../app/models/client';
import { Notifications } from '../../app/notifications/client';
export const isSyncReady = new ReactiveVar(false);
function updateUser(userData) {
const user = Users.findOne({ _id: userData._id });
if (!user || !user._updatedAt || userData._updatedAt > user._updatedAt.toISOString()) {
userData._updatedAt = new Date(userData._updatedAt);
return Meteor.users.upsert({ _id: userData._id }, userData);
}
// delete data already on user's collection as those are newer
Object.keys(user).forEach((key) => delete userData[key]);
Meteor.users.update({ _id: user._id }, { $set: userData });
}
const onUserEvents = {
inserted: (_id, data) => Meteor.users.insert(data),
updated: (_id, { diff }) => Meteor.users.upsert({ _id }, { $set: diff }),
removed: (_id) => Meteor.users.remove({ _id }),
};
export const syncUserdata = async (uid) => {
if (!uid) {
return;
}
await Notifications.onUser('userData', ({ type, id, ...data }) => onUserEvents[type](uid, data));
const userData = await APIClient.v1.get('me');
if (userData) {
updateUser(userData);
}
isSyncReady.set(true);
return userData;
};

@ -0,0 +1,71 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Meteor } from 'meteor/meteor';
import { APIClient } from '../../app/utils/client';
import { Users } from '../../app/models/client';
import { Notifications } from '../../app/notifications/client';
import type { IUser } from '../../definition/IUser';
export const isSyncReady = new ReactiveVar(false);
type RawUserData = Omit<IUser, '_updatedAt'> & {
_updatedAt: string;
};
const updateUser = (userData: IUser & { _updatedAt: Date }): void => {
const user: IUser = Users.findOne({ _id: userData._id });
if (!user || !user._updatedAt || (user._updatedAt.getTime() < userData._updatedAt.getTime())) {
Meteor.users.upsert({ _id: userData._id }, userData);
return;
}
// delete data already on user's collection as those are newer
Object.keys(user).forEach((key) => {
delete userData[key as keyof IUser];
});
Meteor.users.update({ _id: user._id }, { $set: userData });
};
type UserDataNotification = {
id: unknown;
}
& (
({ type: 'inserted' } & Meteor.User)
| ({ type: 'updated' } & Partial<Meteor.User>)
| ({ type: 'removed' })
)
export const synchronizeUserData = async (uid: Meteor.User['_id']): Promise<unknown> => {
if (!uid) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
await Notifications.onUser('userData', ({ type, id, ...data }: UserDataNotification) => {
switch (type) {
case 'inserted':
Meteor.users.insert(data);
break;
case 'updated':
Meteor.users.upsert({ _id: uid }, { $set: data });
break;
case 'removed':
Meteor.users.remove({ _id: uid });
break;
}
});
const userData: RawUserData = await APIClient.v1.get('me');
if (userData) {
updateUser({
...userData,
_updatedAt: new Date(userData._updatedAt),
});
}
isSyncReady.set(true);
return userData;
};

@ -7,7 +7,6 @@ import '../imports/startup/client';
import '../lib/RegExp';
import '../ee/client';
import './lib/toastr';
import './templateHelpers';
import './methods/deleteMessage';
import './methods/hideRoom';

@ -11,7 +11,7 @@ import { UserInfo } from '../../components/basic/UserInfo';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental';
import { FormSkeleton } from './Skeleton';
import { useForm } from '../../hooks/useForm';
import { getUserEmailAddress } from '../../helpers/getUserEmailAddress';
import { getUserEmailAddress } from '../../lib/getUserEmailAddress';
import { useRoute } from '../../contexts/RouterContext';
import { formsSubscription } from '../additionalForms';

@ -14,7 +14,7 @@ import AgentsOverview from './overviews/AgentsOverview';
import ChatsOverview from './overviews/ChatsOverview';
import ProductivityOverview from './overviews/ProductivityOverview';
import DepartmentAutoComplete from '../DepartmentAutoComplete';
import { getDateRange } from '../../helpers/getDateRange';
import { getDateRange } from '../../lib/getDateRange';
import { useTranslation } from '../../contexts/TranslationContext';
const dateRange = getDateRange();

@ -1,7 +1,7 @@
import { HTML } from 'meteor/htmljs';
import { createTemplateForComponent } from '../reactAdapters';
import { createRouteGroup } from '../helpers/createRouteGroup';
import { createRouteGroup } from '../lib/createRouteGroup';
createTemplateForComponent('omnichannelFlex', () => import('./sidebar/OmnichannelSidebar'), {
renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }), // eslint-disable-line new-cap

@ -1,5 +1,5 @@
import { hasPermission } from '../../app/authorization/client';
import { createSidebarItems } from '../helpers/createSidebarItems';
import { createSidebarItems } from '../lib/createSidebarItems';
export const {
registerSidebarItem: registerOmnichannelSidebarItem,

@ -11,7 +11,7 @@ import hljs from '../../app/markdown/lib/hljs';
import { fireGlobalEvent, alerts } from '../../app/ui-utils';
import { getUserPreference, t } from '../../app/utils';
import 'highlight.js/styles/github.css';
import { syncUserdata } from '../lib/userData';
import { synchronizeUserData } from '../lib/userData';
hljs.initHighlightingOnLoad();
@ -42,7 +42,7 @@ Meteor.startup(function() {
return;
}
const user = await syncUserdata(uid);
const user = await synchronizeUserData(uid);
if (!user) {
return;
}

@ -7,10 +7,7 @@ import { useEndpointData } from '../../../../../../client/hooks/useEndpointData'
import Growth from '../../../../../../client/components/data/Growth';
import { Section } from '../Section';
import { ActionButton } from '../../../../../../client/components/basic/Buttons/ActionButton';
import { saveFile } from '../../../../../../client/lib/saveFile';
const convertDataToCSV = (data) => `// type, name, messagesCount, updatedAt, createdAt
${ data.map(({ createdAt, messagesCount, name, t, updatedAt }) => `${ t }, ${ name }, ${ messagesCount }, ${ updatedAt }, ${ createdAt }`).join('\n') }`;
import { downloadCsvAs } from '../../../../../../client/lib/download';
export function TableSection() {
const t = useTranslation();
@ -79,7 +76,14 @@ export function TableSection() {
}, [data]);
const downloadData = () => {
saveFile(convertDataToCSV(channels), `Channels_start_${ params.start }_end_${ params.end }.csv`);
const data = channels.map(({
createdAt,
messagesCount,
name,
t,
updatedAt,
}) => [t, name, messagesCount, updatedAt, createdAt]);
downloadCsvAs(data, `Channels_start_${ params.start }_end_${ params.end }`);
};
return <Section filter={<><Select options={periodOptions} value={periodId} onChange={handlePeriodChange} /><ActionButton mis='x16' disabled={!channels} onClick={downloadData} aria-label={t('Download_Info')} icon='download'/></>}>

@ -8,10 +8,7 @@ import { useEndpointData } from '../../../../../../client/hooks/useEndpointData'
import { LegendSymbol } from '../data/LegendSymbol';
import { Section } from '../Section';
import { ActionButton } from '../../../../../../client/components/basic/Buttons/ActionButton';
import { saveFile } from '../../../../../../client/lib/saveFile';
const convertDataToCSV = (data) => `// type, messagesSent
${ data.map(({ t, messages }) => `${ t }, ${ messages }`).join('\n') }`;
import { downloadCsvAs } from '../../../../../../client/lib/download';
export function MessagesPerChannelSection() {
const t = useTranslation();
@ -70,7 +67,8 @@ export function MessagesPerChannelSection() {
}, [period, pieData, tableData]);
const downloadData = () => {
saveFile(convertDataToCSV(pieData.origins), `MessagesPerChannelSection_start_${ params.start }_end_${ params.end }.csv`);
const data = pieData.origins.map(({ t, messages }) => [t, messages]);
downloadCsvAs(data, `MessagesPerChannelSection_start_${ params.start }_end_${ params.end }`);
};

@ -8,10 +8,7 @@ import { useEndpointData } from '../../../../../../client/hooks/useEndpointData'
import CounterSet from '../../../../../../client/components/data/CounterSet';
import { Section } from '../Section';
import { ActionButton } from '../../../../../../client/components/basic/Buttons/ActionButton';
import { saveFile } from '../../../../../../client/lib/saveFile';
const convertDataToCSV = (data) => `// date, newMessages
${ data.map(({ date, newMessages }) => `${ date }, ${ newMessages }`).join('\n') }`;
import { downloadCsvAs } from '../../../../../../client/lib/download';
export function MessagesSentSection() {
const t = useTranslation();
@ -87,7 +84,8 @@ export function MessagesSentSection() {
}, [data, period]);
const downloadData = () => {
saveFile(convertDataToCSV(values), `MessagesSentSection_start_${ params.start }_end_${ params.end }.csv`);
const data = values.map(({ date, newMessages }) => [date, newMessages]);
downloadCsvAs(data, `MessagesSentSection_start_${ params.start }_end_${ params.end }`);
};
return <Section

@ -9,10 +9,7 @@ import CounterSet from '../../../../../../client/components/data/CounterSet';
import { LegendSymbol } from '../data/LegendSymbol';
import { Section } from '../Section';
import { ActionButton } from '../../../../../../client/components/basic/Buttons/ActionButton';
import { saveFile } from '../../../../../../client/lib/saveFile';
const convertDataToCSV = ({ countDailyActiveUsers, diffDailyActiveUsers, countWeeklyActiveUsers, diffWeeklyActiveUsers, countMonthlyActiveUsers, diffMonthlyActiveUsers, dauValues, wauValues, mauValues }) => `// countDailyActiveUsers, diffDailyActiveUsers, countWeeklyActiveUsers, diffWeeklyActiveUsers, countMonthlyActiveUsers, diffMonthlyActiveUsers, dauValues, wauValues, mauValues
${ countDailyActiveUsers }, ${ diffDailyActiveUsers }, ${ countWeeklyActiveUsers }, ${ diffWeeklyActiveUsers }, ${ countMonthlyActiveUsers }, ${ diffMonthlyActiveUsers }, ${ dauValues }, ${ wauValues }, ${ mauValues }`;
import { downloadCsvAs } from '../../../../../../client/lib/download';
export function ActiveUsersSection() {
const t = useTranslation();
@ -97,7 +94,7 @@ export function ActiveUsersSection() {
}, [period, data]);
const downloadData = () => {
saveFile(convertDataToCSV({
const data = [{
countDailyActiveUsers,
diffDailyActiveUsers,
countWeeklyActiveUsers,
@ -107,7 +104,8 @@ export function ActiveUsersSection() {
dauValues,
wauValues,
mauValues,
}), `ActiveUsersSection_start_${ params.start }_end_${ params.end }.csv`);
}];
downloadCsvAs(data, `ActiveUsersSection_start_${ params.start }_end_${ params.end }`);
};

@ -8,10 +8,7 @@ import { useEndpointData } from '../../../../../../client/hooks/useEndpointData'
import CounterSet from '../../../../../../client/components/data/CounterSet';
import { Section } from '../Section';
import { ActionButton } from '../../../../../../client/components/basic/Buttons/ActionButton';
import { saveFile } from '../../../../../../client/lib/saveFile';
const convertDataToCSV = (data) => `// date, newUsers
${ data.map(({ date, newUsers }) => `${ date }, ${ newUsers }`).join('\n') }`;
import { downloadCsvAs } from '../../../../../../client/lib/download';
export function NewUsersSection() {
const t = useTranslation();
@ -87,7 +84,8 @@ export function NewUsersSection() {
}, [data, period]);
const downloadData = () => {
saveFile(convertDataToCSV(values), `NewUsersSection_start_${ params.start }_end_${ params.end }.csv`);
const data = values.map(({ data, newUsers }) => [data, newUsers]);
downloadCsvAs(data, `NewUsersSection_start_${ params.start }_end_${ params.end }`);
};
return <Section

@ -7,11 +7,7 @@ import { useTranslation } from '../../../../../../client/contexts/TranslationCon
import { useEndpointData } from '../../../../../../client/hooks/useEndpointData';
import { Section } from '../Section';
import { ActionButton } from '../../../../../../client/components/basic/Buttons/ActionButton';
import { saveFile } from '../../../../../../client/lib/saveFile';
const convertDataToCSV = (data) => `// date, users
${ data.map(({ users, hour, day, month, year }) => ({ date: moment([year, month - 1, day, hour, 0, 0, 0]), users })).sort((a, b) => a > b).map(({ date, users }) => `${ date.toISOString() }, ${ users }`).join('\n') }`;
import { downloadCsvAs } from '../../../../../../client/lib/download';
export function UsersByTimeOfTheDaySection() {
const t = useTranslation();
@ -84,7 +80,19 @@ export function UsersByTimeOfTheDaySection() {
}, [data, period.end, period.start]);
const downloadData = () => {
saveFile(convertDataToCSV(data.week), `UsersByTimeOfTheDaySection_start_${ params.start }_end_${ params.end }.csv`);
const _data = data.week.map(({
users,
hour,
day,
month,
year,
}) => ({
date: moment([year, month - 1, day, hour, 0, 0, 0]),
users,
}))
.sort((a, b) => a > b)
.map(({ date, users }) => [date.toISOString(), users]);
downloadCsvAs(_data, `UsersByTimeOfTheDaySection_start_${ params.start }_end_${ params.end }`);
};
return <Section
title={t('Users_by_time_of_day')}

@ -12,5 +12,6 @@ module.exports = {
spec: [
'app/**/*.tests.js',
'app/**/*.tests.ts',
'client/**/*.spec.ts',
],
};

463
package-lock.json generated

@ -9085,9 +9085,9 @@
"dev": true
},
"abab": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz",
"integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ=="
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
"integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q=="
},
"abbrev": {
"version": "1.1.1",
@ -9114,15 +9114,25 @@
"acorn": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
"integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA=="
"integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
"dev": true
},
"acorn-globals": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz",
"integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz",
"integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==",
"dev": true,
"requires": {
"acorn": "^6.0.1",
"acorn-walk": "^6.0.1"
"acorn": "^7.1.1",
"acorn-walk": "^7.1.1"
},
"dependencies": {
"acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true
}
}
},
"acorn-jsx": {
@ -9132,9 +9142,10 @@
"dev": true
},
"acorn-walk": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz",
"integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA=="
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
"dev": true
},
"address": {
"version": "1.1.2",
@ -15016,16 +15027,26 @@
}
},
"cssom": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
"integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==",
"dev": true
},
"cssstyle": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz",
"integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
"integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
"dev": true,
"requires": {
"cssom": "0.3.x"
"cssom": "~0.3.6"
},
"dependencies": {
"cssom": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"dev": true
}
}
},
"csstype": {
@ -15331,25 +15352,14 @@
}
},
"data-urls": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz",
"integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
"integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==",
"dev": true,
"requires": {
"abab": "^2.0.0",
"whatwg-mimetype": "^2.2.0",
"whatwg-url": "^7.0.0"
},
"dependencies": {
"whatwg-url": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
"integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==",
"requires": {
"lodash.sortby": "^4.7.0",
"tr46": "^1.0.1",
"webidl-conversions": "^4.0.2"
}
}
"abab": "^2.0.3",
"whatwg-mimetype": "^2.3.0",
"whatwg-url": "^8.0.0"
}
},
"datauri": {
@ -15430,6 +15440,12 @@
}
}
},
"decimal.js": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz",
"integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==",
"dev": true
},
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
@ -15753,11 +15769,20 @@
"integrity": "sha512-SQVCLFS2E7G5CRCMdn6K9bIhRj1bS6QBWZfF0TUPh4V/BbqrQ619IdSS3/izn0FZ+9l+uODzaZjb08fjOfablA=="
},
"domexception": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
"integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz",
"integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==",
"dev": true,
"requires": {
"webidl-conversions": "^4.0.2"
"webidl-conversions": "^5.0.0"
},
"dependencies": {
"webidl-conversions": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
"integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==",
"dev": true
}
}
},
"domhandler": {
@ -20087,11 +20112,12 @@
"integrity": "sha512-2Uu2W0s8+dnqXzdlg0MRsRzPoDCs1wVjOGSyMRRaMzLDX4bgHw6xDYKccsWafXPPxQpkQfEjgW6+17pwcg60bw=="
},
"html-encoding-sniffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz",
"integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
"integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==",
"dev": true,
"requires": {
"whatwg-encoding": "^1.0.1"
"whatwg-encoding": "^1.0.5"
}
},
"html-entities": {
@ -21365,6 +21391,12 @@
"isobject": "^3.0.1"
}
},
"is-potential-custom-element-name": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz",
"integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=",
"dev": true
},
"is-promise": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
@ -21663,50 +21695,116 @@
"integrity": "sha512-xYuhvQ7I9PDJIGBWev9xm0+SMSed3ZDBAmvVjbFR1ZRLAF+vlXcQu6cRI9uAlj81rzikElRVteehwV7DuX2ZmQ=="
},
"jsdom": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz",
"integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==",
"requires": {
"abab": "^2.0.0",
"acorn": "^5.5.3",
"acorn-globals": "^4.1.0",
"array-equal": "^1.0.0",
"cssom": ">= 0.3.2 < 0.4.0",
"cssstyle": "^1.0.0",
"data-urls": "^1.0.0",
"domexception": "^1.0.1",
"escodegen": "^1.9.1",
"html-encoding-sniffer": "^1.0.2",
"left-pad": "^1.3.0",
"nwsapi": "^2.0.7",
"parse5": "4.0.0",
"pn": "^1.1.0",
"request": "^2.87.0",
"request-promise-native": "^1.0.5",
"sax": "^1.2.4",
"symbol-tree": "^3.2.2",
"tough-cookie": "^2.3.4",
"w3c-hr-time": "^1.0.1",
"webidl-conversions": "^4.0.2",
"whatwg-encoding": "^1.0.3",
"whatwg-mimetype": "^2.1.0",
"whatwg-url": "^6.4.1",
"ws": "^5.2.0",
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz",
"integrity": "sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==",
"dev": true,
"requires": {
"abab": "^2.0.3",
"acorn": "^7.1.1",
"acorn-globals": "^6.0.0",
"cssom": "^0.4.4",
"cssstyle": "^2.2.0",
"data-urls": "^2.0.0",
"decimal.js": "^10.2.0",
"domexception": "^2.0.1",
"escodegen": "^1.14.1",
"html-encoding-sniffer": "^2.0.1",
"is-potential-custom-element-name": "^1.0.0",
"nwsapi": "^2.2.0",
"parse5": "5.1.1",
"request": "^2.88.2",
"request-promise-native": "^1.0.8",
"saxes": "^5.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^3.0.1",
"w3c-hr-time": "^1.0.2",
"w3c-xmlserializer": "^2.0.0",
"webidl-conversions": "^6.1.0",
"whatwg-encoding": "^1.0.5",
"whatwg-mimetype": "^2.3.0",
"whatwg-url": "^8.0.0",
"ws": "^7.2.3",
"xml-name-validator": "^3.0.0"
},
"dependencies": {
"acorn": {
"version": "5.7.4",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz",
"integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg=="
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
"ip-regex": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
"integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=",
"dev": true
},
"request": {
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"dev": true,
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.3",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
},
"dependencies": {
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"dev": true,
"requires": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
}
}
},
"tough-cookie": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
"integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==",
"dev": true,
"requires": {
"ip-regex": "^2.1.0",
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
},
"ws": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
"integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==",
"dev": true
}
}
},
"jsdom-global": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/jsdom-global/-/jsdom-global-3.0.2.tgz",
"integrity": "sha1-a9KZwTsMRiay2iwDk81DhdYGrLk=",
"dev": true
},
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@ -26241,9 +26339,10 @@
}
},
"parse5": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
"integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
"integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
"dev": true
},
"parseurl": {
"version": "1.3.2",
@ -29914,6 +30013,15 @@
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
"integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o="
},
"saxes": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
"integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
"dev": true,
"requires": {
"xmlchars": "^2.2.0"
}
},
"scheduler": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
@ -32721,11 +32829,12 @@
}
},
"tr46": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
"integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz",
"integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==",
"dev": true,
"requires": {
"punycode": "^2.1.0"
"punycode": "^2.1.1"
}
},
"trim": {
@ -32989,6 +33098,151 @@
"integrity": "sha512-popfGXEiedpq6F5saRIAThKxq/bbEPVFnsDnUdjaDGIre9f3/OL9Yi/yPbPcZ7RYUDpekghr666bBfZPrwNnhQ==",
"requires": {
"jsdom": "^11.9.0"
},
"dependencies": {
"acorn": {
"version": "5.7.4",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz",
"integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg=="
},
"acorn-globals": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz",
"integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==",
"requires": {
"acorn": "^6.0.1",
"acorn-walk": "^6.0.1"
},
"dependencies": {
"acorn": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz",
"integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ=="
}
}
},
"acorn-walk": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz",
"integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA=="
},
"cssom": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
},
"cssstyle": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz",
"integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==",
"requires": {
"cssom": "0.3.x"
}
},
"data-urls": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz",
"integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==",
"requires": {
"abab": "^2.0.0",
"whatwg-mimetype": "^2.2.0",
"whatwg-url": "^7.0.0"
},
"dependencies": {
"whatwg-url": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
"integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==",
"requires": {
"lodash.sortby": "^4.7.0",
"tr46": "^1.0.1",
"webidl-conversions": "^4.0.2"
}
}
}
},
"domexception": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
"integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==",
"requires": {
"webidl-conversions": "^4.0.2"
}
},
"html-encoding-sniffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz",
"integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==",
"requires": {
"whatwg-encoding": "^1.0.1"
}
},
"jsdom": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz",
"integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==",
"requires": {
"abab": "^2.0.0",
"acorn": "^5.5.3",
"acorn-globals": "^4.1.0",
"array-equal": "^1.0.0",
"cssom": ">= 0.3.2 < 0.4.0",
"cssstyle": "^1.0.0",
"data-urls": "^1.0.0",
"domexception": "^1.0.1",
"escodegen": "^1.9.1",
"html-encoding-sniffer": "^1.0.2",
"left-pad": "^1.3.0",
"nwsapi": "^2.0.7",
"parse5": "4.0.0",
"pn": "^1.1.0",
"request": "^2.87.0",
"request-promise-native": "^1.0.5",
"sax": "^1.2.4",
"symbol-tree": "^3.2.2",
"tough-cookie": "^2.3.4",
"w3c-hr-time": "^1.0.1",
"webidl-conversions": "^4.0.2",
"whatwg-encoding": "^1.0.3",
"whatwg-mimetype": "^2.1.0",
"whatwg-url": "^6.4.1",
"ws": "^5.2.0",
"xml-name-validator": "^3.0.0"
}
},
"parse5": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
"integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"tr46": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
"integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=",
"requires": {
"punycode": "^2.1.0"
}
},
"webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
"integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="
},
"whatwg-url": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz",
"integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==",
"requires": {
"lodash.sortby": "^4.7.0",
"tr46": "^1.0.1",
"webidl-conversions": "^4.0.2"
}
}
}
},
"tweetnacl": {
@ -33857,6 +34111,15 @@
"browser-process-hrtime": "^1.0.0"
}
},
"w3c-xmlserializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
"integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==",
"dev": true,
"requires": {
"xml-name-validator": "^3.0.0"
}
},
"walkdir": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz",
@ -34103,9 +34366,10 @@
}
},
"webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
"integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
"integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==",
"dev": true
},
"webpack": {
"version": "4.44.1",
@ -34381,13 +34645,14 @@
"integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g=="
},
"whatwg-url": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz",
"integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==",
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz",
"integrity": "sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw==",
"dev": true,
"requires": {
"lodash.sortby": "^4.7.0",
"tr46": "^1.0.1",
"webidl-conversions": "^4.0.2"
"tr46": "^2.0.2",
"webidl-conversions": "^6.1.0"
}
},
"which": {
@ -34656,6 +34921,12 @@
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz",
"integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ=="
},
"xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true
},
"xmldom": {
"version": "0.1.31",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz",

@ -96,6 +96,8 @@
"husky": "^1.2.0",
"i18next": "^17.3.1",
"inquirer": "^7.3.3",
"jsdom": "16.4.0",
"jsdom-global": "3.0.2",
"less-loader": "^5.0.0",
"md5": "^2.3.0",
"mocha": "^8.1.1",

Loading…
Cancel
Save