From d23ccfcd4412c411edab1dbd1c4b186f130921ba Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Fri, 16 Jul 2021 03:42:14 -0700 Subject: [PATCH] Restore copy on text Summary: Some folks were missing the copy as text ManagedTable used to have, so introduced both the options to either copy as text (visible columns or custom copy handler) or as JSON Changelog: It is now possible to both copy as text or as JSON from data tables Reviewed By: jknoxville Differential Revision: D29712096 fbshipit-source-id: 27bd2e869a247bd0896ce2774c08651123fd531d --- desktop/app/src/NotificationsHub.tsx | 2 +- desktop/app/src/dispatcher/notifications.tsx | 2 +- desktop/app/src/index.tsx | 2 +- .../src/ui/components/filter/FilterRow.tsx | 2 +- .../components/searchable/SearchableTable.tsx | 2 +- .../src/ui/components/table/ManagedTable.tsx | 3 +- desktop/app/src/utils/index.tsx | 1 - .../flipper-plugin/src/__tests__/api.node.tsx | 1 + desktop/flipper-plugin/src/index.ts | 2 + .../src/ui/data-table/TableContextMenu.tsx | 113 +++++++++++++----- .../src/ui/data-table/TableRow.tsx | 22 +++- .../src/utils/textContent.tsx | 4 +- docs/extending/flipper-plugin.mdx | 4 + 13 files changed, 116 insertions(+), 44 deletions(-) rename desktop/{app => flipper-plugin}/src/utils/textContent.tsx (95%) diff --git a/desktop/app/src/NotificationsHub.tsx b/desktop/app/src/NotificationsHub.tsx index b3c0c76ab..e08acb8b8 100644 --- a/desktop/app/src/NotificationsHub.tsx +++ b/desktop/app/src/NotificationsHub.tsx @@ -31,7 +31,7 @@ import { } from './reducers/notifications'; import {selectPlugin} from './reducers/connections'; import {State as StoreState} from './reducers/index'; -import textContent from './utils/textContent'; +import {textContent} from 'flipper-plugin'; import createPaste from './fb-stubs/createPaste'; import {getPluginTitle} from './utils/pluginUtils'; import {getFlipperLib} from 'flipper-plugin'; diff --git a/desktop/app/src/dispatcher/notifications.tsx b/desktop/app/src/dispatcher/notifications.tsx index 2a6eadbd4..1f5861383 100644 --- a/desktop/app/src/dispatcher/notifications.tsx +++ b/desktop/app/src/dispatcher/notifications.tsx @@ -15,7 +15,7 @@ import { updatePluginBlocklist, updateCategoryBlocklist, } from '../reducers/notifications'; -import {textContent} from '../utils/index'; +import {textContent} from 'flipper-plugin'; import {getPluginTitle} from '../utils/pluginUtils'; import {sideEffect} from '../utils/sideEffect'; import {openNotification} from '../sandy-chrome/notification/Notification'; diff --git a/desktop/app/src/index.tsx b/desktop/app/src/index.tsx index 23d10123c..5e1f33bf4 100644 --- a/desktop/app/src/index.tsx +++ b/desktop/app/src/index.tsx @@ -12,7 +12,7 @@ export {keyframes} from '@emotion/css'; export {produce} from 'immer'; export * from './ui/index'; -export {textContent, sleep} from './utils/index'; +export {textContent, sleep} from 'flipper-plugin'; export * from './utils/jsonTypes'; export {default as GK, loadGKs, loadDistilleryGK} from './fb-stubs/GK'; export {default as createPaste} from './fb-stubs/createPaste'; diff --git a/desktop/app/src/ui/components/filter/FilterRow.tsx b/desktop/app/src/ui/components/filter/FilterRow.tsx index 80c2fd44a..45a55d67e 100644 --- a/desktop/app/src/ui/components/filter/FilterRow.tsx +++ b/desktop/app/src/ui/components/filter/FilterRow.tsx @@ -10,7 +10,7 @@ import {Filter} from './types'; import React, {PureComponent} from 'react'; import ContextMenu from '../ContextMenu'; -import textContent from '../../../utils/textContent'; +import {textContent} from 'flipper-plugin'; import styled from '@emotion/styled'; import {colors} from '../colors'; diff --git a/desktop/app/src/ui/components/searchable/SearchableTable.tsx b/desktop/app/src/ui/components/searchable/SearchableTable.tsx index f1cc59370..4a5240210 100644 --- a/desktop/app/src/ui/components/searchable/SearchableTable.tsx +++ b/desktop/app/src/ui/components/searchable/SearchableTable.tsx @@ -12,7 +12,7 @@ import ManagedTable, {ManagedTableProps} from '../table/ManagedTable'; import {TableBodyRow} from '../table/types'; import Searchable, {SearchableProps} from './Searchable'; import React, {PureComponent} from 'react'; -import textContent from '../../../utils/textContent'; +import {textContent} from 'flipper-plugin'; import deepEqual from 'deep-equal'; type Props = { diff --git a/desktop/app/src/ui/components/table/ManagedTable.tsx b/desktop/app/src/ui/components/table/ManagedTable.tsx index 641c59614..3c99bb54b 100644 --- a/desktop/app/src/ui/components/table/ManagedTable.tsx +++ b/desktop/app/src/ui/components/table/ManagedTable.tsx @@ -31,9 +31,8 @@ import createPaste from '../../../fb-stubs/createPaste'; import debounceRender from 'react-debounce-render'; import {debounce} from 'lodash'; import {DEFAULT_ROW_HEIGHT} from './types'; -import textContent from '../../../utils/textContent'; import {notNull} from '../../../utils/typeUtils'; -import {getFlipperLib} from 'flipper-plugin'; +import {getFlipperLib, textContent} from 'flipper-plugin'; const EMPTY_OBJECT = {}; Object.freeze(EMPTY_OBJECT); diff --git a/desktop/app/src/utils/index.tsx b/desktop/app/src/utils/index.tsx index 1d6a71790..6d5c796a3 100644 --- a/desktop/app/src/utils/index.tsx +++ b/desktop/app/src/utils/index.tsx @@ -7,6 +7,5 @@ * @format */ -export {default as textContent} from './textContent'; export {getStringFromErrorLike} from './errors'; export {sleep} from './promiseTimeout'; diff --git a/desktop/flipper-plugin/src/__tests__/api.node.tsx b/desktop/flipper-plugin/src/__tests__/api.node.tsx index b93d87c69..9a1e7eb94 100644 --- a/desktop/flipper-plugin/src/__tests__/api.node.tsx +++ b/desktop/flipper-plugin/src/__tests__/api.node.tsx @@ -57,6 +57,7 @@ test('Correct top level API exposed', () => { "renderReactRoot", "sleep", "styled", + "textContent", "theme", "useLocalStorageState", "useLogger", diff --git a/desktop/flipper-plugin/src/index.ts b/desktop/flipper-plugin/src/index.ts index 8dd008651..7fd1b0561 100644 --- a/desktop/flipper-plugin/src/index.ts +++ b/desktop/flipper-plugin/src/index.ts @@ -132,6 +132,8 @@ export { export {createTablePlugin} from './utils/createTablePlugin'; +export {textContent} from './utils/textContent'; + // It's not ideal that this exists in flipper-plugin sources directly, // but is the least pain for plugin authors. // Probably we should make sure that testing-library doesn't end up in our final Flipper bundle (which packages flipper-plugin) diff --git a/desktop/flipper-plugin/src/ui/data-table/TableContextMenu.tsx b/desktop/flipper-plugin/src/ui/data-table/TableContextMenu.tsx index 5a030c9e2..7a9cb068c 100644 --- a/desktop/flipper-plugin/src/ui/data-table/TableContextMenu.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/TableContextMenu.tsx @@ -7,7 +7,7 @@ * @format */ -import {CopyOutlined, FilterOutlined} from '@ant-design/icons'; +import {CopyOutlined, FilterOutlined, TableOutlined} from '@ant-design/icons'; import {Checkbox, Menu} from 'antd'; import { DataTableDispatch, @@ -20,20 +20,21 @@ import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib'; import {DataTableColumn} from './DataTable'; import {toFirstUpper} from '../../utils/toFirstUpper'; import {DataSource} from '../../data-source/index'; +import {renderColumnValue} from './TableRow'; +import {textContent} from '../../utils/textContent'; const {Item, SubMenu} = Menu; -function defaultOnCopyRows(items: T[]) { - return JSON.stringify(items.length > 1 ? items : items[0], null, 2); -} - export function tableContextMenuFactory( datasource: DataSource, dispatch: DataTableDispatch, selection: Selection, columns: DataTableColumn[], visibleColumns: DataTableColumn[], - onCopyRows: (rows: T[]) => string = defaultOnCopyRows, + onCopyRows: ( + rows: T[], + visibleColumns: DataTableColumn[], + ) => string = defaultOnCopyRows, onContextMenu?: (selection: undefined | T) => React.ReactElement, ) { const lib = tryGetFlipperLibImplementation(); @@ -68,6 +69,61 @@ export function tableContextMenuFactory( ))} + } + disabled={!hasSelection}> + { + const items = getSelectedItems(datasource, selection); + if (items.length) { + lib.writeTextToClipboard(onCopyRows(items, visibleColumns)); + } + }}> + Copy row(s) + + {lib.isFB && ( + { + const items = getSelectedItems(datasource, selection); + if (items.length) { + lib.createPaste(onCopyRows(items, visibleColumns)); + } + }}> + Create paste + + )} + { + const items = getSelectedItems(datasource, selection); + if (items.length) { + lib.writeTextToClipboard(rowsToJson(items)); + } + }}> + Copy row(s) (JSON) + + {lib.isFB && ( + { + const items = getSelectedItems(datasource, selection); + if (items.length) { + lib.createPaste(rowsToJson(items)); + } + }}> + Create paste (JSON) + + )} + + ( ))} - { - const items = getSelectedItems(datasource, selection); - if (items.length) { - lib.writeTextToClipboard(onCopyRows(items)); - } - }}> - Copy row(s) - - {lib.isFB && ( - { - const items = getSelectedItems(datasource, selection); - if (items.length) { - lib.createPaste(onCopyRows(items)); - } - }}> - Create paste - - )} {columns.map((column, idx) => ( @@ -143,3 +175,24 @@ function friendlyColumnTitle(column: DataTableColumn): string { const name = column.title || column.key; return toFirstUpper(name); } + +function defaultOnCopyRows( + items: T[], + visibleColumns: DataTableColumn[], +) { + return ( + visibleColumns.map(friendlyColumnTitle).join('\t') + + '\n' + + items + .map((row, idx) => + visibleColumns + .map((col) => textContent(renderColumnValue(col, row, true, idx))) + .join('\t'), + ) + .join('\n') + ); +} + +function rowsToJson(items: T[]) { + return JSON.stringify(items.length > 1 ? items : items[0], null, 2); +} diff --git a/desktop/flipper-plugin/src/ui/data-table/TableRow.tsx b/desktop/flipper-plugin/src/ui/data-table/TableRow.tsx index 00e804409..86ac104e4 100644 --- a/desktop/flipper-plugin/src/ui/data-table/TableRow.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/TableRow.tsx @@ -10,7 +10,7 @@ import React, {CSSProperties, memo} from 'react'; import styled from '@emotion/styled'; import {theme} from '../theme'; -import type {TableRowRenderContext} from './DataTable'; +import {DataTableColumn, TableRowRenderContext} from './DataTable'; import {Width} from '../../utils/widthUtils'; import {DataFormatter} from '../DataFormatter'; import {Dropdown} from 'antd'; @@ -121,9 +121,12 @@ export const TableRow = memo(function TableRow({ {config.columns .filter((col) => col.visible) .map((col) => { - const value = col.onRender - ? (col as any).onRender(record, highlighted, itemIndex) - : DataFormatter.format((record as any)[col.key], col.formatters); + const value = renderColumnValue( + col, + record, + highlighted, + itemIndex, + ); return ( ({ return row; } }); + +export function renderColumnValue( + col: DataTableColumn, + record: T, + highlighted: boolean, + itemIndex: number, +) { + return col.onRender + ? col.onRender(record, highlighted, itemIndex) + : DataFormatter.format((record as any)[col.key], col.formatters); +} diff --git a/desktop/app/src/utils/textContent.tsx b/desktop/flipper-plugin/src/utils/textContent.tsx similarity index 95% rename from desktop/app/src/utils/textContent.tsx rename to desktop/flipper-plugin/src/utils/textContent.tsx index 889d24c8d..a9628bab2 100644 --- a/desktop/app/src/utils/textContent.tsx +++ b/desktop/flipper-plugin/src/utils/textContent.tsx @@ -21,7 +21,7 @@ function isReactElement(object: any) { * Recursively walks through all children of a React element and returns * the string representation of the leafs concatenated. */ -export default (node: ReactNode): string => { +export function textContent(node: ReactNode): string { let res = ''; const traverse = (node: ReactNode) => { if (typeof node === 'string' || typeof node === 'number') { @@ -44,4 +44,4 @@ export default (node: ReactNode): string => { traverse(node); return res; -}; +} diff --git a/docs/extending/flipper-plugin.mdx b/docs/extending/flipper-plugin.mdx index 679e77b2e..cca46bc13 100644 --- a/docs/extending/flipper-plugin.mdx +++ b/docs/extending/flipper-plugin.mdx @@ -1012,6 +1012,10 @@ Creates a promise that automatically resolves after the specified amount of mill A convenience re-export of `styled` from [emotion](https://emotion.sh/docs/styled). +## textContent + +Given a string or React element, returns a text representation of that element, that is suitable as plain text. + ## TestUtils The object `TestUtils` as exposed from `flipper-plugin` exposes utilities to write unit tests for Sandy plugins.