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
This commit is contained in:
Michel Weststrate
2021-07-16 03:42:14 -07:00
committed by Facebook GitHub Bot
parent 1e93055eb5
commit d23ccfcd44
13 changed files with 116 additions and 44 deletions

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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 = {

View File

@@ -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);

View File

@@ -7,6 +7,5 @@
* @format
*/
export {default as textContent} from './textContent';
export {getStringFromErrorLike} from './errors';
export {sleep} from './promiseTimeout';

View File

@@ -57,6 +57,7 @@ test('Correct top level API exposed', () => {
"renderReactRoot",
"sleep",
"styled",
"textContent",
"theme",
"useLocalStorageState",
"useLogger",

View File

@@ -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)

View File

@@ -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<T>(items: T[]) {
return JSON.stringify(items.length > 1 ? items : items[0], null, 2);
}
export function tableContextMenuFactory<T>(
datasource: DataSource<T>,
dispatch: DataTableDispatch<T>,
selection: Selection,
columns: DataTableColumn<T>[],
visibleColumns: DataTableColumn<T>[],
onCopyRows: (rows: T[]) => string = defaultOnCopyRows,
onCopyRows: (
rows: T[],
visibleColumns: DataTableColumn<T>[],
) => string = defaultOnCopyRows,
onContextMenu?: (selection: undefined | T) => React.ReactElement,
) {
const lib = tryGetFlipperLibImplementation();
@@ -68,6 +69,61 @@ export function tableContextMenuFactory<T>(
</Item>
))}
</SubMenu>
<SubMenu
key="copy rows"
title="Copy row(s)"
icon={<TableOutlined />}
disabled={!hasSelection}>
<Item
key="copyToClipboard"
disabled={!hasSelection}
onClick={() => {
const items = getSelectedItems(datasource, selection);
if (items.length) {
lib.writeTextToClipboard(onCopyRows(items, visibleColumns));
}
}}>
Copy row(s)
</Item>
{lib.isFB && (
<Item
key="createPaste"
disabled={!hasSelection}
onClick={() => {
const items = getSelectedItems(datasource, selection);
if (items.length) {
lib.createPaste(onCopyRows(items, visibleColumns));
}
}}>
Create paste
</Item>
)}
<Item
key="copyToClipboardJSON"
disabled={!hasSelection}
onClick={() => {
const items = getSelectedItems(datasource, selection);
if (items.length) {
lib.writeTextToClipboard(rowsToJson(items));
}
}}>
Copy row(s) (JSON)
</Item>
{lib.isFB && (
<Item
key="createPaste"
disabled={!hasSelection}
onClick={() => {
const items = getSelectedItems(datasource, selection);
if (items.length) {
lib.createPaste(rowsToJson(items));
}
}}>
Create paste (JSON)
</Item>
)}
</SubMenu>
<SubMenu
key="copy cells"
title="Copy cell(s)"
@@ -88,30 +144,6 @@ export function tableContextMenuFactory<T>(
</Item>
))}
</SubMenu>
<Item
key="copyToClipboard"
disabled={!hasSelection}
onClick={() => {
const items = getSelectedItems(datasource, selection);
if (items.length) {
lib.writeTextToClipboard(onCopyRows(items));
}
}}>
Copy row(s)
</Item>
{lib.isFB && (
<Item
key="createPaste"
disabled={!hasSelection}
onClick={() => {
const items = getSelectedItems(datasource, selection);
if (items.length) {
lib.createPaste(onCopyRows(items));
}
}}>
Create paste
</Item>
)}
<Menu.Divider />
<SubMenu title="Visible columns" key="visible columns">
{columns.map((column, idx) => (
@@ -143,3 +175,24 @@ function friendlyColumnTitle(column: DataTableColumn<any>): string {
const name = column.title || column.key;
return toFirstUpper(name);
}
function defaultOnCopyRows<T>(
items: T[],
visibleColumns: DataTableColumn<T>[],
) {
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<T>(items: T[]) {
return JSON.stringify(items.length > 1 ? items : items[0], null, 2);
}

View File

@@ -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<T>({
{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<T>(
col,
record,
highlighted,
itemIndex,
);
return (
<TableBodyColumnContainer
@@ -147,3 +150,14 @@ export const TableRow = memo(function TableRow<T>({
return row;
}
});
export function renderColumnValue<T>(
col: DataTableColumn<any>,
record: T,
highlighted: boolean,
itemIndex: number,
) {
return col.onRender
? col.onRender(record, highlighted, itemIndex)
: DataFormatter.format((record as any)[col.key], col.formatters);
}

View File

@@ -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;
};
}

View File

@@ -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.