From 8187be1b7dd723fec9591e2fc4f57a782f3ca398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Mon, 9 Oct 2023 17:43:57 +0200 Subject: [PATCH] feat(editor): Make PDF and Audio binary-data viewable in the UI (#7367) fixes #7361 --- .../src/components/BinaryDataDisplayEmbed.vue | 7 +- packages/editor-ui/src/components/RunData.vue | 8 +- .../src/components/__tests__/RunData.test.ts | 145 ++++++++++++------ .../test/ReadPDF-encrypted.workflow.json | 1 + .../nodes/ReadPdf/test/ReadPDF.workflow.json | 1 + packages/workflow/src/Interfaces.ts | 2 +- packages/workflow/src/utils.ts | 4 +- packages/workflow/test/utils.test.ts | 40 ++++- 8 files changed, 150 insertions(+), 58 deletions(-) diff --git a/packages/editor-ui/src/components/BinaryDataDisplayEmbed.vue b/packages/editor-ui/src/components/BinaryDataDisplayEmbed.vue index 0a0f12480ffa8..98073dff096c5 100644 --- a/packages/editor-ui/src/components/BinaryDataDisplayEmbed.vue +++ b/packages/editor-ui/src/components/BinaryDataDisplayEmbed.vue @@ -7,6 +7,10 @@ {{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }} + -
+
{{ key }}
@@ -432,7 +432,7 @@ v-if="isViewable(index, key)" size="small" :label="$locale.baseText('runData.showBinaryData')" - class="binary-data-show-data-button" + data-test-id="ndv-view-binary-data" @click="displayBinaryData(index, key)" />
@@ -1320,7 +1320,7 @@ export default defineComponent({ }, isViewable(index: number, key: string): boolean { const { fileType } = this.binaryData[index][key]; - return !!fileType && ['image', 'video', 'text', 'json'].includes(fileType); + return !!fileType && ['image', 'audio', 'video', 'text', 'json', 'pdf'].includes(fileType); }, isDownloadable(index: number, key: string): boolean { const { mimeType, fileName } = this.binaryData[index][key]; diff --git a/packages/editor-ui/src/components/__tests__/RunData.test.ts b/packages/editor-ui/src/components/__tests__/RunData.test.ts index cb3b417868c53..4af1d9292acc5 100644 --- a/packages/editor-ui/src/components/__tests__/RunData.test.ts +++ b/packages/editor-ui/src/components/__tests__/RunData.test.ts @@ -6,30 +6,102 @@ import RunData from '@/components/RunData.vue'; import { STORES, VIEWS } from '@/constants'; import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; import { createComponentRenderer } from '@/__tests__/render'; - -const renderComponent = createComponentRenderer(RunData, { - props: { - nodeUi: { - name: 'Test Node', - }, - }, - data() { - return { - canPinData: true, - }; - }, - global: { - mocks: { - $route: { - name: VIEWS.WORKFLOW, - }, - }, - }, -}); +import type { IRunDataDisplayMode } from '@/Interface'; describe('RunData', () => { it('should render data correctly even when "item.json" has another "json" key', async () => { - const { html, getByText, getAllByTestId, getByTestId } = renderComponent({ + const { getByText, getAllByTestId, getByTestId } = render( + [ + { + json: { + id: 1, + name: 'Test 1', + json: { + data: 'Json data 1', + }, + }, + }, + { + json: { + id: 2, + name: 'Test 2', + json: { + data: 'Json data 2', + }, + }, + }, + ], + 'schema', + ); + + await userEvent.click(getByTestId('ndv-pin-data')); + await waitFor(() => getAllByTestId('run-data-schema-item'), { timeout: 1000 }); + expect(getByText('Test 1')).toBeInTheDocument(); + expect(getByText('Json data 1')).toBeInTheDocument(); + }); + + it('should render view and download buttons for PDFs', async () => { + const { getByTestId } = render( + [ + { + json: {}, + binary: { + data: { + fileName: 'test.pdf', + fileType: 'pdf', + mimeType: 'application/pdf', + }, + }, + }, + ], + 'binary', + ); + expect(getByTestId('ndv-view-binary-data')).toBeInTheDocument(); + expect(getByTestId('ndv-download-binary-data')).toBeInTheDocument(); + expect(getByTestId('ndv-binary-data_0')).toBeInTheDocument(); + }); + + it('should not render a view button for unknown content-type', async () => { + const { getByTestId, queryByTestId } = render( + [ + { + json: {}, + binary: { + data: { + fileName: 'test.xyz', + mimeType: 'application/octet-stream', + }, + }, + }, + ], + 'binary', + ); + expect(queryByTestId('ndv-view-binary-data')).not.toBeInTheDocument(); + expect(getByTestId('ndv-download-binary-data')).toBeInTheDocument(); + expect(getByTestId('ndv-binary-data_0')).toBeInTheDocument(); + }); + + const render = (outputData: unknown[], displayMode: IRunDataDisplayMode) => + createComponentRenderer(RunData, { + props: { + nodeUi: { + name: 'Test Node', + }, + }, + data() { + return { + canPinData: true, + showData: true, + }; + }, + global: { + mocks: { + $route: { + name: VIEWS.WORKFLOW, + }, + }, + }, + })({ props: { nodeUi: { id: '1', @@ -49,7 +121,7 @@ describe('RunData', () => { }, [STORES.NDV]: { output: { - displayMode: 'schema', + displayMode, }, activeNodeName: 'Test Node', }, @@ -89,28 +161,7 @@ describe('RunData', () => { startTime: new Date().getTime(), executionTime: new Date().getTime(), data: { - main: [ - [ - { - json: { - id: 1, - name: 'Test 1', - json: { - data: 'Json data 1', - }, - }, - }, - { - json: { - id: 2, - name: 'Test 2', - json: { - data: 'Json data 2', - }, - }, - }, - ], - ], + main: [outputData], }, source: [null], }, @@ -123,10 +174,4 @@ describe('RunData', () => { }, }), }); - - await userEvent.click(getByTestId('ndv-pin-data')); - await waitFor(() => getAllByTestId('run-data-schema-item'), { timeout: 1000 }); - expect(getByText('Test 1')).toBeInTheDocument(); - expect(getByText('Json data 1')).toBeInTheDocument(); - }); }); diff --git a/packages/nodes-base/nodes/ReadPdf/test/ReadPDF-encrypted.workflow.json b/packages/nodes-base/nodes/ReadPdf/test/ReadPDF-encrypted.workflow.json index 7258819ead32b..1bf9f597b819b 100644 --- a/packages/nodes-base/nodes/ReadPdf/test/ReadPDF-encrypted.workflow.json +++ b/packages/nodes-base/nodes/ReadPdf/test/ReadPDF-encrypted.workflow.json @@ -35,6 +35,7 @@ "fileExtension": "pdf", "fileName": "sample-encrypted.pdf", "fileSize": "18.9 kB", + "fileType": "pdf", "mimeType": "application/pdf" } }, diff --git a/packages/nodes-base/nodes/ReadPdf/test/ReadPDF.workflow.json b/packages/nodes-base/nodes/ReadPdf/test/ReadPDF.workflow.json index 4a5664010842e..5b257711addc4 100644 --- a/packages/nodes-base/nodes/ReadPdf/test/ReadPDF.workflow.json +++ b/packages/nodes-base/nodes/ReadPdf/test/ReadPDF.workflow.json @@ -32,6 +32,7 @@ "fileExtension": "pdf", "fileName": "sample.pdf", "fileSize": "17.8 kB", + "fileType": "pdf", "mimeType": "application/pdf" } }, diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 1256a1df25bd6..4624c303cde48 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -35,7 +35,7 @@ export type IAllExecuteFunctions = | ITriggerFunctions | IWebhookFunctions; -export type BinaryFileType = 'text' | 'json' | 'image' | 'video'; +export type BinaryFileType = 'text' | 'json' | 'image' | 'audio' | 'video' | 'pdf'; export interface IBinaryData { [key: string]: string | undefined; data: string; diff --git a/packages/workflow/src/utils.ts b/packages/workflow/src/utils.ts index 27d96b4dcbaf6..e6c95b0023d4e 100644 --- a/packages/workflow/src/utils.ts +++ b/packages/workflow/src/utils.ts @@ -113,8 +113,10 @@ export const sleep = async (ms: number): Promise => export function fileTypeFromMimeType(mimeType: string): BinaryFileType | undefined { if (mimeType.startsWith('application/json')) return 'json'; if (mimeType.startsWith('image/')) return 'image'; + if (mimeType.startsWith('audio/')) return 'audio'; if (mimeType.startsWith('video/')) return 'video'; - if (mimeType.startsWith('text/')) return 'text'; + if (mimeType.startsWith('text/') || mimeType.startsWith('application/javascript')) return 'text'; + if (mimeType.startsWith('application/pdf')) return 'pdf'; return; } diff --git a/packages/workflow/test/utils.test.ts b/packages/workflow/test/utils.test.ts index e26d964976620..f8cbd007f52b1 100644 --- a/packages/workflow/test/utils.test.ts +++ b/packages/workflow/test/utils.test.ts @@ -1,4 +1,4 @@ -import { jsonParse, jsonStringify, deepCopy, isObjectEmpty } from '@/utils'; +import { jsonParse, jsonStringify, deepCopy, isObjectEmpty, fileTypeFromMimeType } from '@/utils'; describe('isObjectEmpty', () => { it('should handle null and undefined', () => { @@ -190,3 +190,41 @@ describe('deepCopy', () => { expect(copy.deep.arr.slice(-1)[0]).not.toBe(object); }); }); + +describe('fileTypeFromMimeType', () => { + it('should recognize json', () => { + expect(fileTypeFromMimeType('application/json')).toEqual('json'); + }); + + it('should recognize image', () => { + expect(fileTypeFromMimeType('image/jpeg')).toEqual('image'); + expect(fileTypeFromMimeType('image/png')).toEqual('image'); + expect(fileTypeFromMimeType('image/avif')).toEqual('image'); + expect(fileTypeFromMimeType('image/webp')).toEqual('image'); + }); + + it('should recognize audio', () => { + expect(fileTypeFromMimeType('audio/wav')).toEqual('audio'); + expect(fileTypeFromMimeType('audio/webm')).toEqual('audio'); + expect(fileTypeFromMimeType('audio/ogg')).toEqual('audio'); + expect(fileTypeFromMimeType('audio/mp3')).toEqual('audio'); + }); + + it('should recognize video', () => { + expect(fileTypeFromMimeType('video/mp4')).toEqual('video'); + expect(fileTypeFromMimeType('video/webm')).toEqual('video'); + expect(fileTypeFromMimeType('video/ogg')).toEqual('video'); + }); + + it('should recognize text', () => { + expect(fileTypeFromMimeType('text/plain')).toEqual('text'); + expect(fileTypeFromMimeType('text/css')).toEqual('text'); + expect(fileTypeFromMimeType('text/html')).toEqual('text'); + expect(fileTypeFromMimeType('text/javascript')).toEqual('text'); + expect(fileTypeFromMimeType('application/javascript')).toEqual('text'); + }); + + it('should recognize pdf', () => { + expect(fileTypeFromMimeType('application/pdf')).toEqual('pdf'); + }); +});