mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Make PDF and Audio binary-data viewable in the UI (#7367)
fixes #7361
This commit is contained in:
parent
732b15a1fa
commit
8187be1b7d
|
@ -7,6 +7,10 @@
|
||||||
<source :src="embedSource" :type="binaryData.mimeType" />
|
<source :src="embedSource" :type="binaryData.mimeType" />
|
||||||
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
|
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
|
||||||
</video>
|
</video>
|
||||||
|
<audio v-if="binaryData.fileType === 'audio'" controls autoplay>
|
||||||
|
<source :src="embedSource" :type="binaryData.mimeType" />
|
||||||
|
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
|
||||||
|
</audio>
|
||||||
<vue-json-pretty
|
<vue-json-pretty
|
||||||
v-else-if="binaryData.fileType === 'json'"
|
v-else-if="binaryData.fileType === 'json'"
|
||||||
:data="jsonData"
|
:data="jsonData"
|
||||||
|
@ -92,7 +96,8 @@ export default defineComponent({
|
||||||
max-width: calc(100% - 1em);
|
max-width: calc(100% - 1em);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.other {
|
&.other,
|
||||||
|
&.pdf {
|
||||||
height: calc(100% - 1em);
|
height: calc(100% - 1em);
|
||||||
width: calc(100% - 1em);
|
width: calc(100% - 1em);
|
||||||
}
|
}
|
||||||
|
|
|
@ -382,7 +382,7 @@
|
||||||
v-for="(binaryData, key) in binaryDataEntry"
|
v-for="(binaryData, key) in binaryDataEntry"
|
||||||
:key="index + '_' + key"
|
:key="index + '_' + key"
|
||||||
>
|
>
|
||||||
<div>
|
<div :data-test-id="'ndv-binary-data_' + index">
|
||||||
<div :class="$style.binaryHeader">
|
<div :class="$style.binaryHeader">
|
||||||
{{ key }}
|
{{ key }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -432,7 +432,7 @@
|
||||||
v-if="isViewable(index, key)"
|
v-if="isViewable(index, key)"
|
||||||
size="small"
|
size="small"
|
||||||
:label="$locale.baseText('runData.showBinaryData')"
|
:label="$locale.baseText('runData.showBinaryData')"
|
||||||
class="binary-data-show-data-button"
|
data-test-id="ndv-view-binary-data"
|
||||||
@click="displayBinaryData(index, key)"
|
@click="displayBinaryData(index, key)"
|
||||||
/>
|
/>
|
||||||
<n8n-button
|
<n8n-button
|
||||||
|
@ -440,7 +440,7 @@
|
||||||
size="small"
|
size="small"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
:label="$locale.baseText('runData.downloadBinaryData')"
|
:label="$locale.baseText('runData.downloadBinaryData')"
|
||||||
class="binary-data-show-data-button"
|
data-test-id="ndv-download-binary-data"
|
||||||
@click="downloadBinaryData(index, key)"
|
@click="downloadBinaryData(index, key)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1320,7 +1320,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
isViewable(index: number, key: string): boolean {
|
isViewable(index: number, key: string): boolean {
|
||||||
const { fileType } = this.binaryData[index][key];
|
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 {
|
isDownloadable(index: number, key: string): boolean {
|
||||||
const { mimeType, fileName } = this.binaryData[index][key];
|
const { mimeType, fileName } = this.binaryData[index][key];
|
||||||
|
|
|
@ -6,8 +6,83 @@ import RunData from '@/components/RunData.vue';
|
||||||
import { STORES, VIEWS } from '@/constants';
|
import { STORES, VIEWS } from '@/constants';
|
||||||
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import type { IRunDataDisplayMode } from '@/Interface';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(RunData, {
|
describe('RunData', () => {
|
||||||
|
it('should render data correctly even when "item.json" has another "json" key', async () => {
|
||||||
|
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: {
|
props: {
|
||||||
nodeUi: {
|
nodeUi: {
|
||||||
name: 'Test Node',
|
name: 'Test Node',
|
||||||
|
@ -16,6 +91,7 @@ const renderComponent = createComponentRenderer(RunData, {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
canPinData: true,
|
canPinData: true,
|
||||||
|
showData: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
|
@ -25,11 +101,7 @@ const renderComponent = createComponentRenderer(RunData, {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})({
|
||||||
|
|
||||||
describe('RunData', () => {
|
|
||||||
it('should render data correctly even when "item.json" has another "json" key', async () => {
|
|
||||||
const { html, getByText, getAllByTestId, getByTestId } = renderComponent({
|
|
||||||
props: {
|
props: {
|
||||||
nodeUi: {
|
nodeUi: {
|
||||||
id: '1',
|
id: '1',
|
||||||
|
@ -49,7 +121,7 @@ describe('RunData', () => {
|
||||||
},
|
},
|
||||||
[STORES.NDV]: {
|
[STORES.NDV]: {
|
||||||
output: {
|
output: {
|
||||||
displayMode: 'schema',
|
displayMode,
|
||||||
},
|
},
|
||||||
activeNodeName: 'Test Node',
|
activeNodeName: 'Test Node',
|
||||||
},
|
},
|
||||||
|
@ -89,28 +161,7 @@ describe('RunData', () => {
|
||||||
startTime: new Date().getTime(),
|
startTime: new Date().getTime(),
|
||||||
executionTime: new Date().getTime(),
|
executionTime: new Date().getTime(),
|
||||||
data: {
|
data: {
|
||||||
main: [
|
main: [outputData],
|
||||||
[
|
|
||||||
{
|
|
||||||
json: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Test 1',
|
|
||||||
json: {
|
|
||||||
data: 'Json data 1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
json: {
|
|
||||||
id: 2,
|
|
||||||
name: 'Test 2',
|
|
||||||
json: {
|
|
||||||
data: 'Json data 2',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
source: [null],
|
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
"fileExtension": "pdf",
|
"fileExtension": "pdf",
|
||||||
"fileName": "sample-encrypted.pdf",
|
"fileName": "sample-encrypted.pdf",
|
||||||
"fileSize": "18.9 kB",
|
"fileSize": "18.9 kB",
|
||||||
|
"fileType": "pdf",
|
||||||
"mimeType": "application/pdf"
|
"mimeType": "application/pdf"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"fileExtension": "pdf",
|
"fileExtension": "pdf",
|
||||||
"fileName": "sample.pdf",
|
"fileName": "sample.pdf",
|
||||||
"fileSize": "17.8 kB",
|
"fileSize": "17.8 kB",
|
||||||
|
"fileType": "pdf",
|
||||||
"mimeType": "application/pdf"
|
"mimeType": "application/pdf"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -35,7 +35,7 @@ export type IAllExecuteFunctions =
|
||||||
| ITriggerFunctions
|
| ITriggerFunctions
|
||||||
| IWebhookFunctions;
|
| IWebhookFunctions;
|
||||||
|
|
||||||
export type BinaryFileType = 'text' | 'json' | 'image' | 'video';
|
export type BinaryFileType = 'text' | 'json' | 'image' | 'audio' | 'video' | 'pdf';
|
||||||
export interface IBinaryData {
|
export interface IBinaryData {
|
||||||
[key: string]: string | undefined;
|
[key: string]: string | undefined;
|
||||||
data: string;
|
data: string;
|
||||||
|
|
|
@ -113,8 +113,10 @@ export const sleep = async (ms: number): Promise<void> =>
|
||||||
export function fileTypeFromMimeType(mimeType: string): BinaryFileType | undefined {
|
export function fileTypeFromMimeType(mimeType: string): BinaryFileType | undefined {
|
||||||
if (mimeType.startsWith('application/json')) return 'json';
|
if (mimeType.startsWith('application/json')) return 'json';
|
||||||
if (mimeType.startsWith('image/')) return 'image';
|
if (mimeType.startsWith('image/')) return 'image';
|
||||||
|
if (mimeType.startsWith('audio/')) return 'audio';
|
||||||
if (mimeType.startsWith('video/')) return 'video';
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { jsonParse, jsonStringify, deepCopy, isObjectEmpty } from '@/utils';
|
import { jsonParse, jsonStringify, deepCopy, isObjectEmpty, fileTypeFromMimeType } from '@/utils';
|
||||||
|
|
||||||
describe('isObjectEmpty', () => {
|
describe('isObjectEmpty', () => {
|
||||||
it('should handle null and undefined', () => {
|
it('should handle null and undefined', () => {
|
||||||
|
@ -190,3 +190,41 @@ describe('deepCopy', () => {
|
||||||
expect(copy.deep.arr.slice(-1)[0]).not.toBe(object);
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in a new issue