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:
committed by
Facebook GitHub Bot
parent
1e93055eb5
commit
d23ccfcd44
@@ -31,7 +31,7 @@ import {
|
|||||||
} from './reducers/notifications';
|
} from './reducers/notifications';
|
||||||
import {selectPlugin} from './reducers/connections';
|
import {selectPlugin} from './reducers/connections';
|
||||||
import {State as StoreState} from './reducers/index';
|
import {State as StoreState} from './reducers/index';
|
||||||
import textContent from './utils/textContent';
|
import {textContent} from 'flipper-plugin';
|
||||||
import createPaste from './fb-stubs/createPaste';
|
import createPaste from './fb-stubs/createPaste';
|
||||||
import {getPluginTitle} from './utils/pluginUtils';
|
import {getPluginTitle} from './utils/pluginUtils';
|
||||||
import {getFlipperLib} from 'flipper-plugin';
|
import {getFlipperLib} from 'flipper-plugin';
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
updatePluginBlocklist,
|
updatePluginBlocklist,
|
||||||
updateCategoryBlocklist,
|
updateCategoryBlocklist,
|
||||||
} from '../reducers/notifications';
|
} from '../reducers/notifications';
|
||||||
import {textContent} from '../utils/index';
|
import {textContent} from 'flipper-plugin';
|
||||||
import {getPluginTitle} from '../utils/pluginUtils';
|
import {getPluginTitle} from '../utils/pluginUtils';
|
||||||
import {sideEffect} from '../utils/sideEffect';
|
import {sideEffect} from '../utils/sideEffect';
|
||||||
import {openNotification} from '../sandy-chrome/notification/Notification';
|
import {openNotification} from '../sandy-chrome/notification/Notification';
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export {keyframes} from '@emotion/css';
|
|||||||
export {produce} from 'immer';
|
export {produce} from 'immer';
|
||||||
|
|
||||||
export * from './ui/index';
|
export * from './ui/index';
|
||||||
export {textContent, sleep} from './utils/index';
|
export {textContent, sleep} from 'flipper-plugin';
|
||||||
export * from './utils/jsonTypes';
|
export * from './utils/jsonTypes';
|
||||||
export {default as GK, loadGKs, loadDistilleryGK} from './fb-stubs/GK';
|
export {default as GK, loadGKs, loadDistilleryGK} from './fb-stubs/GK';
|
||||||
export {default as createPaste} from './fb-stubs/createPaste';
|
export {default as createPaste} from './fb-stubs/createPaste';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import {Filter} from './types';
|
import {Filter} from './types';
|
||||||
import React, {PureComponent} from 'react';
|
import React, {PureComponent} from 'react';
|
||||||
import ContextMenu from '../ContextMenu';
|
import ContextMenu from '../ContextMenu';
|
||||||
import textContent from '../../../utils/textContent';
|
import {textContent} from 'flipper-plugin';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {colors} from '../colors';
|
import {colors} from '../colors';
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import ManagedTable, {ManagedTableProps} from '../table/ManagedTable';
|
|||||||
import {TableBodyRow} from '../table/types';
|
import {TableBodyRow} from '../table/types';
|
||||||
import Searchable, {SearchableProps} from './Searchable';
|
import Searchable, {SearchableProps} from './Searchable';
|
||||||
import React, {PureComponent} from 'react';
|
import React, {PureComponent} from 'react';
|
||||||
import textContent from '../../../utils/textContent';
|
import {textContent} from 'flipper-plugin';
|
||||||
import deepEqual from 'deep-equal';
|
import deepEqual from 'deep-equal';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -31,9 +31,8 @@ import createPaste from '../../../fb-stubs/createPaste';
|
|||||||
import debounceRender from 'react-debounce-render';
|
import debounceRender from 'react-debounce-render';
|
||||||
import {debounce} from 'lodash';
|
import {debounce} from 'lodash';
|
||||||
import {DEFAULT_ROW_HEIGHT} from './types';
|
import {DEFAULT_ROW_HEIGHT} from './types';
|
||||||
import textContent from '../../../utils/textContent';
|
|
||||||
import {notNull} from '../../../utils/typeUtils';
|
import {notNull} from '../../../utils/typeUtils';
|
||||||
import {getFlipperLib} from 'flipper-plugin';
|
import {getFlipperLib, textContent} from 'flipper-plugin';
|
||||||
|
|
||||||
const EMPTY_OBJECT = {};
|
const EMPTY_OBJECT = {};
|
||||||
Object.freeze(EMPTY_OBJECT);
|
Object.freeze(EMPTY_OBJECT);
|
||||||
|
|||||||
@@ -7,6 +7,5 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export {default as textContent} from './textContent';
|
|
||||||
export {getStringFromErrorLike} from './errors';
|
export {getStringFromErrorLike} from './errors';
|
||||||
export {sleep} from './promiseTimeout';
|
export {sleep} from './promiseTimeout';
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ test('Correct top level API exposed', () => {
|
|||||||
"renderReactRoot",
|
"renderReactRoot",
|
||||||
"sleep",
|
"sleep",
|
||||||
"styled",
|
"styled",
|
||||||
|
"textContent",
|
||||||
"theme",
|
"theme",
|
||||||
"useLocalStorageState",
|
"useLocalStorageState",
|
||||||
"useLogger",
|
"useLogger",
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ export {
|
|||||||
|
|
||||||
export {createTablePlugin} from './utils/createTablePlugin';
|
export {createTablePlugin} from './utils/createTablePlugin';
|
||||||
|
|
||||||
|
export {textContent} from './utils/textContent';
|
||||||
|
|
||||||
// It's not ideal that this exists in flipper-plugin sources directly,
|
// It's not ideal that this exists in flipper-plugin sources directly,
|
||||||
// but is the least pain for plugin authors.
|
// 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)
|
// Probably we should make sure that testing-library doesn't end up in our final Flipper bundle (which packages flipper-plugin)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {CopyOutlined, FilterOutlined} from '@ant-design/icons';
|
import {CopyOutlined, FilterOutlined, TableOutlined} from '@ant-design/icons';
|
||||||
import {Checkbox, Menu} from 'antd';
|
import {Checkbox, Menu} from 'antd';
|
||||||
import {
|
import {
|
||||||
DataTableDispatch,
|
DataTableDispatch,
|
||||||
@@ -20,20 +20,21 @@ import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
|
|||||||
import {DataTableColumn} from './DataTable';
|
import {DataTableColumn} from './DataTable';
|
||||||
import {toFirstUpper} from '../../utils/toFirstUpper';
|
import {toFirstUpper} from '../../utils/toFirstUpper';
|
||||||
import {DataSource} from '../../data-source/index';
|
import {DataSource} from '../../data-source/index';
|
||||||
|
import {renderColumnValue} from './TableRow';
|
||||||
|
import {textContent} from '../../utils/textContent';
|
||||||
|
|
||||||
const {Item, SubMenu} = Menu;
|
const {Item, SubMenu} = Menu;
|
||||||
|
|
||||||
function defaultOnCopyRows<T>(items: T[]) {
|
|
||||||
return JSON.stringify(items.length > 1 ? items : items[0], null, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tableContextMenuFactory<T>(
|
export function tableContextMenuFactory<T>(
|
||||||
datasource: DataSource<T>,
|
datasource: DataSource<T>,
|
||||||
dispatch: DataTableDispatch<T>,
|
dispatch: DataTableDispatch<T>,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
columns: DataTableColumn<T>[],
|
columns: DataTableColumn<T>[],
|
||||||
visibleColumns: DataTableColumn<T>[],
|
visibleColumns: DataTableColumn<T>[],
|
||||||
onCopyRows: (rows: T[]) => string = defaultOnCopyRows,
|
onCopyRows: (
|
||||||
|
rows: T[],
|
||||||
|
visibleColumns: DataTableColumn<T>[],
|
||||||
|
) => string = defaultOnCopyRows,
|
||||||
onContextMenu?: (selection: undefined | T) => React.ReactElement,
|
onContextMenu?: (selection: undefined | T) => React.ReactElement,
|
||||||
) {
|
) {
|
||||||
const lib = tryGetFlipperLibImplementation();
|
const lib = tryGetFlipperLibImplementation();
|
||||||
@@ -68,6 +69,61 @@ export function tableContextMenuFactory<T>(
|
|||||||
</Item>
|
</Item>
|
||||||
))}
|
))}
|
||||||
</SubMenu>
|
</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
|
<SubMenu
|
||||||
key="copy cells"
|
key="copy cells"
|
||||||
title="Copy cell(s)"
|
title="Copy cell(s)"
|
||||||
@@ -88,30 +144,6 @@ export function tableContextMenuFactory<T>(
|
|||||||
</Item>
|
</Item>
|
||||||
))}
|
))}
|
||||||
</SubMenu>
|
</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 />
|
<Menu.Divider />
|
||||||
<SubMenu title="Visible columns" key="visible columns">
|
<SubMenu title="Visible columns" key="visible columns">
|
||||||
{columns.map((column, idx) => (
|
{columns.map((column, idx) => (
|
||||||
@@ -143,3 +175,24 @@ function friendlyColumnTitle(column: DataTableColumn<any>): string {
|
|||||||
const name = column.title || column.key;
|
const name = column.title || column.key;
|
||||||
return toFirstUpper(name);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import React, {CSSProperties, memo} from 'react';
|
import React, {CSSProperties, memo} from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {theme} from '../theme';
|
import {theme} from '../theme';
|
||||||
import type {TableRowRenderContext} from './DataTable';
|
import {DataTableColumn, TableRowRenderContext} from './DataTable';
|
||||||
import {Width} from '../../utils/widthUtils';
|
import {Width} from '../../utils/widthUtils';
|
||||||
import {DataFormatter} from '../DataFormatter';
|
import {DataFormatter} from '../DataFormatter';
|
||||||
import {Dropdown} from 'antd';
|
import {Dropdown} from 'antd';
|
||||||
@@ -121,9 +121,12 @@ export const TableRow = memo(function TableRow<T>({
|
|||||||
{config.columns
|
{config.columns
|
||||||
.filter((col) => col.visible)
|
.filter((col) => col.visible)
|
||||||
.map((col) => {
|
.map((col) => {
|
||||||
const value = col.onRender
|
const value = renderColumnValue<T>(
|
||||||
? (col as any).onRender(record, highlighted, itemIndex)
|
col,
|
||||||
: DataFormatter.format((record as any)[col.key], col.formatters);
|
record,
|
||||||
|
highlighted,
|
||||||
|
itemIndex,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableBodyColumnContainer
|
<TableBodyColumnContainer
|
||||||
@@ -147,3 +150,14 @@ export const TableRow = memo(function TableRow<T>({
|
|||||||
return row;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function isReactElement(object: any) {
|
|||||||
* Recursively walks through all children of a React element and returns
|
* Recursively walks through all children of a React element and returns
|
||||||
* the string representation of the leafs concatenated.
|
* the string representation of the leafs concatenated.
|
||||||
*/
|
*/
|
||||||
export default (node: ReactNode): string => {
|
export function textContent(node: ReactNode): string {
|
||||||
let res = '';
|
let res = '';
|
||||||
const traverse = (node: ReactNode) => {
|
const traverse = (node: ReactNode) => {
|
||||||
if (typeof node === 'string' || typeof node === 'number') {
|
if (typeof node === 'string' || typeof node === 'number') {
|
||||||
@@ -44,4 +44,4 @@ export default (node: ReactNode): string => {
|
|||||||
traverse(node);
|
traverse(node);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
}
|
||||||
@@ -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).
|
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
|
## TestUtils
|
||||||
|
|
||||||
The object `TestUtils` as exposed from `flipper-plugin` exposes utilities to write unit tests for Sandy plugins.
|
The object `TestUtils` as exposed from `flipper-plugin` exposes utilities to write unit tests for Sandy plugins.
|
||||||
|
|||||||
Reference in New Issue
Block a user