Commit 151e980c authored by Jacques Erasmus's avatar Jacques Erasmus

Merge branch 'client-side-csv-viewer' into 'master'

Add client-side blob viewer for CSV files

See merge request gitlab-org/gitlab!81292
parents ab1491be ae39be9a
......@@ -63,6 +63,9 @@ export default {
isEmpty() {
return this.blob.rawSize === 0;
},
blobSwitcherDocIcon() {
return this.blob.richViewer?.fileType === 'csv' ? 'table' : 'document';
},
},
watch: {
viewer(newVal, oldVal) {
......@@ -90,7 +93,7 @@ export default {
</div>
<div class="gl-sm-display-flex file-actions">
<viewer-switcher v-if="showViewerSwitcher" v-model="viewer" />
<viewer-switcher v-if="showViewerSwitcher" v-model="viewer" :doc-icon="blobSwitcherDocIcon" />
<slot name="actions"></slot>
......
......@@ -21,6 +21,11 @@ export default {
default: SIMPLE_BLOB_VIEWER,
required: false,
},
docIcon: {
type: String,
default: 'document',
required: false,
},
},
computed: {
isSimpleViewer() {
......@@ -62,7 +67,7 @@ export default {
:aria-label="$options.RICH_BLOB_VIEWER_TITLE"
:title="$options.RICH_BLOB_VIEWER_TITLE"
:selected="isRichViewer"
icon="document"
:icon="docIcon"
category="primary"
variant="default"
class="js-blob-viewer-switch-btn"
......
......@@ -14,6 +14,11 @@ export default {
type: String,
required: true,
},
remoteFile: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -23,14 +28,29 @@ export default {
};
},
mounted() {
const parsed = Papa.parse(this.csv, { skipEmptyLines: true });
this.items = parsed.data;
if (parsed.errors.length) {
this.papaParseErrors = parsed.errors;
if (!this.remoteFile) {
const parsed = Papa.parse(this.csv, { skipEmptyLines: true });
this.handleParsedData(parsed);
} else {
Papa.parse(this.csv, {
download: true,
skipEmptyLines: true,
complete: (parsed) => {
this.handleParsedData(parsed);
},
});
}
},
methods: {
handleParsedData(parsed) {
this.items = parsed.data;
this.loading = false;
if (parsed.errors.length) {
this.papaParseErrors = parsed.errors;
}
this.loading = false;
},
},
};
</script>
......
<script>
import CsvViewer from '~/blob/csv/csv_viewer.vue';
export default {
components: {
CsvViewer,
},
props: {
blob: {
type: Object,
required: true,
},
},
data() {
return {
url: this.blob.rawPath,
};
},
};
</script>
<template>
<div>
<csv-viewer :csv="url" remote-file data-testid="csv" />
</div>
</template>
const viewers = {
csv: () => import('./csv_viewer.vue'),
download: () => import('./download_viewer.vue'),
image: () => import('./image_viewer.vue'),
video: () => import('./video_viewer.vue'),
......
......@@ -21,6 +21,7 @@ exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
class="gl-sm-display-flex file-actions"
>
<viewer-switcher-stub
docicon="document"
value="simple"
/>
......
......@@ -159,5 +159,20 @@ describe('Blob Header Default Actions', () => {
await nextTick();
expect(wrapper.vm.$emit).not.toHaveBeenCalled();
});
it('sets different icons depending on the blob file type', async () => {
factory();
expect(wrapper.vm.blobSwitcherDocIcon).toBe('document');
await wrapper.setProps({
blob: {
...Blob,
richViewer: {
...Blob.richViewer,
fileType: 'csv',
},
},
});
expect(wrapper.vm.blobSwitcherDocIcon).toBe('table');
});
});
});
......@@ -2,6 +2,7 @@ import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { getAllByRole } from '@testing-library/dom';
import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Papa from 'papaparse';
import CsvViewer from '~/blob/csv/csv_viewer.vue';
import PapaParseAlert from '~/vue_shared/components/papa_parse_alert.vue';
......@@ -11,10 +12,15 @@ const brokenCsv = '{\n "json": 1,\n "key": [1, 2, 3]\n}';
describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
let wrapper;
const createComponent = ({ csv = validCsv, mountFunction = shallowMount } = {}) => {
const createComponent = ({
csv = validCsv,
remoteFile = false,
mountFunction = shallowMount,
} = {}) => {
wrapper = mountFunction(CsvViewer, {
propsData: {
csv,
remoteFile,
},
});
};
......@@ -73,4 +79,22 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
expect(getAllByRole(wrapper.element, 'row', { name: /Three/i })).toHaveLength(1);
});
});
describe('when csv prop is path and indicates a remote file', () => {
it('should render call parse with download flag true', async () => {
const path = 'path/to/remote/file.csv';
jest.spyOn(Papa, 'parse').mockImplementation((_, { complete }) => {
complete({ data: validCsv.split(','), errors: [] });
});
createComponent({ csv: path, remoteFile: true });
expect(Papa.parse).toHaveBeenCalledWith(path, {
download: true,
skipEmptyLines: true,
complete: expect.any(Function),
});
await nextTick;
expect(wrapper.vm.items).toEqual(validCsv.split(','));
});
});
});
import { shallowMount } from '@vue/test-utils';
import CsvViewer from '~/repository/components/blob_viewers/csv_viewer.vue';
describe('CSV Viewer', () => {
let wrapper;
const DEFAULT_BLOB_DATA = {
rawPath: 'some/file.csv',
name: 'file.csv',
};
const createComponent = () => {
wrapper = shallowMount(CsvViewer, {
propsData: { blob: DEFAULT_BLOB_DATA },
stubs: ['CsvViewer'],
});
};
const findCsvViewerComp = () => wrapper.find('[data-testid="csv"]');
it('renders a Source Editor component', () => {
createComponent();
expect(findCsvViewerComp().exists()).toBe(true);
expect(findCsvViewerComp().props('remoteFile')).toBeTruthy();
expect(findCsvViewerComp().props('csv')).toBe(DEFAULT_BLOB_DATA.rawPath);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment