From 4534889e6873fddff3ff6c46c2cf19892685a434 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 14 Oct 2020 10:59:28 -0300 Subject: [PATCH 01/11] Convert capitalize() to TypeScript --- client/helpers/capitalize.js | 4 ---- client/helpers/capitalize.ts | 7 +++++++ 2 files changed, 7 insertions(+), 4 deletions(-) delete mode 100644 client/helpers/capitalize.js create mode 100644 client/helpers/capitalize.ts diff --git a/client/helpers/capitalize.js b/client/helpers/capitalize.js deleted file mode 100644 index e709a37503bff..0000000000000 --- a/client/helpers/capitalize.js +++ /dev/null @@ -1,4 +0,0 @@ -export const capitalize = (s) => { - if (typeof s !== 'string') { return ''; } - return s.charAt(0).toUpperCase() + s.slice(1); -}; diff --git a/client/helpers/capitalize.ts b/client/helpers/capitalize.ts new file mode 100644 index 0000000000000..c3fc40dbfdb14 --- /dev/null +++ b/client/helpers/capitalize.ts @@ -0,0 +1,7 @@ +export const capitalize = (s: string): string => { + if (typeof s !== 'string') { + return ''; + } + + return s.charAt(0).toUpperCase() + s.slice(1); +}; From 4b2d8668084b9f7ad0b77f558a8bc1fa7ae5eb2a Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 14 Oct 2020 11:21:12 -0300 Subject: [PATCH 02/11] Convert createRouteGroup() to TypeScript --- .../{createRouteGroup.js => createRouteGroup.ts} | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) rename client/helpers/{createRouteGroup.js => createRouteGroup.ts} (51%) diff --git a/client/helpers/createRouteGroup.js b/client/helpers/createRouteGroup.ts similarity index 51% rename from client/helpers/createRouteGroup.js rename to client/helpers/createRouteGroup.ts index 89b28298aeaee..c25fc1abe81a5 100644 --- a/client/helpers/createRouteGroup.js +++ b/client/helpers/createRouteGroup.ts @@ -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; + props: Record; + action: (params?: Record, queryParams?: Record) => void; + }): void; +}; + +export const createRouteGroup = (name: string, prefix: string, importRouter: () => Promise): 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) => { From d1e0243d76230e421cea0005cfaa548743487fba Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 14 Oct 2020 11:29:09 -0300 Subject: [PATCH 03/11] Convert createSidebarItem() to TypeScript --- client/helpers/createSidebarItems.js | 26 ------------------ client/helpers/createSidebarItems.ts | 41 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 26 deletions(-) delete mode 100644 client/helpers/createSidebarItems.js create mode 100644 client/helpers/createSidebarItems.ts diff --git a/client/helpers/createSidebarItems.js b/client/helpers/createSidebarItems.js deleted file mode 100644 index 8b8a8905890ff..0000000000000 --- a/client/helpers/createSidebarItems.js +++ /dev/null @@ -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 }; -}; diff --git a/client/helpers/createSidebarItems.ts b/client/helpers/createSidebarItems.ts new file mode 100644 index 0000000000000..32cb8025111dd --- /dev/null +++ b/client/helpers/createSidebarItems.ts @@ -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; +}) => { + const items = initialItems; + let updateCb: (() => void) = () => undefined; + + const itemsSubscription: Subscription = { + 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, + }; +}; From b0c38212dbd2116a7942ecbc1c559a44ec6761df Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 14 Oct 2020 16:26:25 -0300 Subject: [PATCH 04/11] Assemble a set of functions to download files programmatically --- .../client/tabs/uploadedFilesList.js | 12 +++--- client/admin/info/InformationRoute.js | 4 +- client/helpers/download.js | 15 ------- client/helpers/download.ts | 43 +++++++++++++++++++ client/lib/saveFile.js | 9 ---- .../components/ChannelsTab/TableSection.js | 14 +++--- .../MessagesTab/MessagesPerChannelSection.js | 8 ++-- .../MessagesTab/MessagesSentSection.js | 8 ++-- .../components/UsersTab/ActiveUsersSection.js | 10 ++--- .../components/UsersTab/NewUsersSection.js | 8 ++-- .../UsersTab/UsersByTimeOfTheDaySection.js | 20 ++++++--- 11 files changed, 86 insertions(+), 65 deletions(-) delete mode 100644 client/helpers/download.js create mode 100644 client/helpers/download.ts delete mode 100644 client/lib/saveFile.js diff --git a/app/ui-flextab/client/tabs/uploadedFilesList.js b/app/ui-flextab/client/tabs/uploadedFilesList.js index 6cd62c94cb700..93e856c9a2827 100644 --- a/app/ui-flextab/client/tabs/uploadedFilesList.js +++ b/app/ui-flextab/client/tabs/uploadedFilesList.js @@ -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/helpers/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); }, }, ], diff --git a/client/admin/info/InformationRoute.js b/client/admin/info/InformationRoute.js index 4f3ae4aba0093..6e14a8eb795b6 100644 --- a/client/admin/info/InformationRoute.js +++ b/client/admin/info/InformationRoute.js @@ -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 '../../helpers/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) { diff --git a/client/helpers/download.js b/client/helpers/download.js deleted file mode 100644 index 089f19cecf171..0000000000000 --- a/client/helpers/download.js +++ /dev/null @@ -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); -}; diff --git a/client/helpers/download.ts b/client/helpers/download.ts new file mode 100644 index 0000000000000..61bfa7d843482 --- /dev/null +++ b/client/helpers/download.ts @@ -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`); +}; diff --git a/client/lib/saveFile.js b/client/lib/saveFile.js deleted file mode 100644 index ff21605daaf27..0000000000000 --- a/client/lib/saveFile.js +++ /dev/null @@ -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(); -}; diff --git a/ee/app/engagement-dashboard/client/components/ChannelsTab/TableSection.js b/ee/app/engagement-dashboard/client/components/ChannelsTab/TableSection.js index 443a2a4abd8e6..37fad9b2c19b9 100644 --- a/ee/app/engagement-dashboard/client/components/ChannelsTab/TableSection.js +++ b/ee/app/engagement-dashboard/client/components/ChannelsTab/TableSection.js @@ -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/helpers/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