From e7bc0717ee018a61eb01dfacccaba44fc5595e09 Mon Sep 17 00:00:00 2001 From: Kaz Wesley Date: Fri, 28 Jun 2024 06:51:03 -0700 Subject: [PATCH] Copied table-viz range pastes as Table component (#10352) When copying from AG Grid, include additional information in the clipboard to enable the data to be pasted into the graph as a new Table component. Closes #10275. # Important Notes - Data copied from the table visualization now include headers. (cherry picked from commit da21136861dbaa1de46c0f6ba8fd948ddcff1b1b) --- CHANGELOG.md | 2 + .../GraphEditor/__tests__/clipboard.test.ts | 12 +-- .../src/components/GraphEditor/clipboard.ts | 93 +++++++++++-------- .../visualizations/TableVisualization.vue | 32 +++++++ 4 files changed, 90 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0b8c74b00a9..abb55a35e657 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ disallowed changing it again.][10337] - [Added click through on table and vector visualisation][10340] clicking on index column will select row or value in seperate node +- [Copied table-viz range pastes as Table component][10352] - [Added support for links in documentation panels][10353]. [10064]: https://github.com/enso-org/enso/pull/10064 @@ -35,6 +36,7 @@ [10327]: https://github.com/enso-org/enso/pull/10327 [10337]: https://github.com/enso-org/enso/pull/10337 [10340]: https://github.com/enso-org/enso/pull/10340 +[10352]: https://github.com/enso-org/enso/pull/10352 [10353]: https://github.com/enso-org/enso/pull/10353 #### Enso Standard Library diff --git a/app/gui2/src/components/GraphEditor/__tests__/clipboard.test.ts b/app/gui2/src/components/GraphEditor/__tests__/clipboard.test.ts index a393ce221a01..64e69acdeefe 100644 --- a/app/gui2/src/components/GraphEditor/__tests__/clipboard.test.ts +++ b/app/gui2/src/components/GraphEditor/__tests__/clipboard.test.ts @@ -3,7 +3,7 @@ import { isSpreadsheetTsv, nodesFromClipboardContent, nodesToClipboardData, - tsvToEnsoTable, + tsvTableToEnsoExpression, } from '@/components/GraphEditor/clipboard' import { type Node } from '@/stores/graph' import { Ast } from '@/util/ast' @@ -44,7 +44,7 @@ test.each([ "'\\t36\\t52\\n11\\t\\t4.727272727\\n12\\t\\t4.333333333\\n13\\t2.769230769\\t4\\n14\\t2.571428571\\t3.714285714\\n15\\t2.4\\t3.466666667\\n16\\t2.25\\t3.25\\n17\\t2.117647059\\t3.058823529\\n19\\t1.894736842\\t2.736842105\\n21\\t1.714285714\\t2.476190476\\n24\\t1.5\\t2.166666667\\n27\\t1.333333333\\t1.925925926\\n30\\t1.2\\t'.to Table", }, ])('Enso expression from Excel data: $description', ({ tableData, expectedEnsoExpression }) => { - expect(tsvToEnsoTable(tableData)).toEqual(expectedEnsoExpression) + expect(tsvTableToEnsoExpression(tableData)).toEqual(expectedEnsoExpression) }) class MockClipboardItem { @@ -83,12 +83,8 @@ const testNodes = testNodeInputs.map(({ code, visualization, colorOverride }) => test.each([...testNodes.map((node) => [node]), testNodes])( 'Copy and paste nodes', async (...sourceNodes) => { - const clipboardItems = nodesToClipboardData( - sourceNodes, - (data) => new MockClipboardItem(data as any) as any, - (parts, type) => new Blob(parts, { type }) as any, - ) - const pastedNodes = await nodesFromClipboardContent(clipboardItems) + const clipboardItem = clipboardItemFromTypes(nodesToClipboardData(sourceNodes)) + const pastedNodes = await nodesFromClipboardContent([clipboardItem]) sourceNodes.forEach((sourceNode, i) => { expect(pastedNodes[i]?.documentation).toBe(sourceNode.documentation) expect(pastedNodes[i]?.expression).toBe(sourceNode.innerExpr.code()) diff --git a/app/gui2/src/components/GraphEditor/clipboard.ts b/app/gui2/src/components/GraphEditor/clipboard.ts index 72a5dbcff841..f277ee0389c6 100644 --- a/app/gui2/src/components/GraphEditor/clipboard.ts +++ b/app/gui2/src/components/GraphEditor/clipboard.ts @@ -26,19 +26,6 @@ interface CopiedNode { metadata?: NodeMetadataFields } -function nodeStructuredData(node: Node): CopiedNode { - return { - expression: node.innerExpr.code(), - documentation: node.documentation, - metadata: node.rootExpr.serializeMetadata(), - ...(node.pattern ? { binding: node.pattern.code() } : {}), - } -} - -function nodeDataFromExpressionText(expression: string): CopiedNode { - return { expression } -} - /** @internal Exported for testing. */ export async function nodesFromClipboardContent( clipboardItems: ClipboardItems, @@ -50,34 +37,20 @@ export async function nodesFromClipboardContent( const ensoDecoder: ClipboardDecoder = { mimeType: ENSO_MIME_TYPE, - decode: async (blob) => JSON.parse(await blob.text()).nodes, + decode: async (blob) => (JSON.parse(await blob.text()) as ClipboardData).nodes, } const plainTextDecoder: ClipboardDecoder = { mimeType: 'text/plain', - decode: async (blob) => [nodeDataFromExpressionText(await blob.text())], + decode: async (blob) => [{ expression: await blob.text() }], } -type clipboardItemFactory = (itemData: Record) => ClipboardItem -type blobFactory = (parts: string[], type: string) => Blob - -/** @internal Exported for testing. */ -export function nodesToClipboardData( - nodes: Node[], - makeClipboardItem: clipboardItemFactory = (data) => new ClipboardItem(data), - makeBlob: blobFactory = (parts, type) => new Blob(parts, { type }), -): ClipboardItem[] { - const clipboardData: ClipboardData = { nodes: nodes.map(nodeStructuredData) } - const jsonItem = makeBlob([JSON.stringify(clipboardData)], ENSO_MIME_TYPE) - const textItem = makeBlob([nodes.map((node) => node.outerExpr.code()).join('\n')], 'text/plain') - return [ - makeClipboardItem({ - [jsonItem.type]: jsonItem, - [textItem.type]: textItem, - }), - ] +interface ExtendedClipboard extends Clipboard { + // Recent addition to the spec: https://github.com/w3c/clipboard-apis/pull/197 + // Currently supported by Chromium: https://developer.chrome.com/docs/web-platform/unsanitized-html-async-clipboard + read(options?: { unsanitized?: ['text/html'] }): Promise } -function getClipboard() { +function getClipboard(): ExtendedClipboard { return (window.navigator as any).mockClipboard ?? window.navigator.clipboard } @@ -87,7 +60,7 @@ export function useGraphEditorClipboard( createNodes: (nodesOptions: Iterable) => void, ) { /** Copy the content of the selected node to the clipboard. */ - function copySelectionToClipboard() { + async function copySelectionToClipboard() { const nodes = new Array() const ids = graphStore.pickInCodeOrder(toValue(selected)) for (const id of ids) { @@ -96,9 +69,7 @@ export function useGraphEditorClipboard( nodes.push(node) } if (!nodes.length) return - getClipboard() - .write(nodesToClipboardData(nodes)) - .catch((error: any) => console.error(`Failed to write to clipboard: ${error}`)) + return writeClipboard(nodesToClipboardData(nodes)) } /** Read the clipboard and if it contains valid data, create nodes from the content. */ @@ -137,7 +108,9 @@ export function useGraphEditorClipboard( } } +// ========================== // === Clipboard decoding === +// ========================== interface ClipboardDecoder { mimeType: string @@ -169,15 +142,14 @@ const spreadsheetDecoder: ClipboardDecoder = { if (!item.types.includes('text/plain')) return if (isSpreadsheetTsv(htmlContent)) { const textData = await item.getType('text/plain').then((blob) => blob.text()) - return [nodeDataFromExpressionText(tsvToEnsoTable(textData))] + return [{ expression: tsvTableToEnsoExpression(textData) }] } }, } const toTable = computed(() => Pattern.parse('__.to Table')) -/** @internal Exported for testing. */ -export function tsvToEnsoTable(tsvData: string) { +export function tsvTableToEnsoExpression(tsvData: string) { const textLiteral = Ast.TextLiteral.new(tsvData) return toTable.value.instantiate(textLiteral.module, [textLiteral]).code() } @@ -191,3 +163,42 @@ export function isSpreadsheetTsv(htmlContent: string) { // acceptable. return /]/i.test(htmlContent) } + +// ========================= +// === Clipboard writing === +// ========================= + +export type MimeType = 'text/plain' | 'text/html' | typeof ENSO_MIME_TYPE +export type MimeData = Partial> + +export function writeClipboard(data: MimeData) { + const dataBlobs = Object.fromEntries( + Object.entries(data).map(([type, typeData]) => [type, new Blob([typeData], { type })]), + ) + return getClipboard() + .write([new ClipboardItem(dataBlobs)]) + .catch((error: any) => console.error(`Failed to write to clipboard: ${error}`)) +} + +// === Serializing nodes === + +function nodeStructuredData(node: Node): CopiedNode { + return { + expression: node.innerExpr.code(), + documentation: node.documentation, + metadata: node.rootExpr.serializeMetadata(), + ...(node.pattern ? { binding: node.pattern.code() } : {}), + } +} + +export function clipboardNodeData(nodes: CopiedNode[]): MimeData { + const clipboardData: ClipboardData = { nodes } + return { [ENSO_MIME_TYPE]: JSON.stringify(clipboardData) } +} + +export function nodesToClipboardData(nodes: Node[]): MimeData { + return { + ...clipboardNodeData(nodes.map(nodeStructuredData)), + 'text/plain': nodes.map((node) => node.outerExpr.code()).join('\n'), + } +} diff --git a/app/gui2/src/components/visualizations/TableVisualization.vue b/app/gui2/src/components/visualizations/TableVisualization.vue index 316c8c8aca0e..795944f5403d 100644 --- a/app/gui2/src/components/visualizations/TableVisualization.vue +++ b/app/gui2/src/components/visualizations/TableVisualization.vue @@ -1,5 +1,10 @@