Commit 91fd2b5d authored by Lee Tickett's avatar Lee Tickett Committed by Mark Florian

Render CSV parsing errors

We need to handle translation of errors returned by the papaparse
csv parsing library and package up a reusable component for
displaying them. This can be resued in the  upcoming diff viewer.

Changelog: added
parent d0c3ad98
<script> <script>
import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui'; import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import Papa from 'papaparse'; import Papa from 'papaparse';
import PapaParseAlert from '~/vue_shared/components/papa_parse_alert.vue';
export default { export default {
components: { components: {
PapaParseAlert,
GlTable, GlTable,
GlAlert,
GlLoadingIcon, GlLoadingIcon,
}, },
props: { props: {
...@@ -17,7 +18,7 @@ export default { ...@@ -17,7 +18,7 @@ export default {
data() { data() {
return { return {
items: [], items: [],
errorMessage: null, papaParseErrors: [],
loading: true, loading: true,
}; };
}, },
...@@ -26,7 +27,7 @@ export default { ...@@ -26,7 +27,7 @@ export default {
this.items = parsed.data; this.items = parsed.data;
if (parsed.errors.length) { if (parsed.errors.length) {
this.errorMessage = parsed.errors.map((e) => e.message).join('. '); this.papaParseErrors = parsed.errors;
} }
this.loading = false; this.loading = false;
...@@ -40,9 +41,7 @@ export default { ...@@ -40,9 +41,7 @@ export default {
<gl-loading-icon class="gl-mt-5" size="lg" /> <gl-loading-icon class="gl-mt-5" size="lg" />
</div> </div>
<div v-else> <div v-else>
<gl-alert v-if="errorMessage" variant="danger" :dismissible="false"> <papa-parse-alert v-if="papaParseErrors.length" :papa-parse-errors="papaParseErrors" />
{{ errorMessage }}
</gl-alert>
<gl-table <gl-table
:empty-text="__('No CSV data to display.')" :empty-text="__('No CSV data to display.')"
:items="items" :items="items"
......
<script>
import { GlAlert } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlAlert,
},
i18n: {
genericErrorMessage: s__('CsvParser|Failed to render the CSV file for the following reasons:'),
MissingQuotes: s__('CsvParser|Quoted field unterminated'),
InvalidQuotes: s__('CsvParser|Trailing quote on quoted field is malformed'),
UndetectableDelimiter: s__('CsvParser|Unable to auto-detect delimiter; defaulted to ","'),
TooManyFields: s__('CsvParser|Too many fields'),
TooFewFields: s__('CsvParser|Too few fields'),
},
props: {
papaParseErrors: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
errorMessages() {
const errorMessages = this.papaParseErrors.map(
(error) => this.$options.i18n[error.code] ?? error.message,
);
return new Set(errorMessages);
},
},
};
</script>
<template>
<gl-alert variant="danger" :dismissible="false">
{{ $options.i18n.genericErrorMessage }}
<ul class="gl-mb-0!">
<li v-for="error in errorMessages" :key="error">
{{ error }}
</li>
</ul>
</gl-alert>
</template>
...@@ -9642,6 +9642,24 @@ msgstr "" ...@@ -9642,6 +9642,24 @@ msgstr ""
msgid "Crowd" msgid "Crowd"
msgstr "" msgstr ""
msgid "CsvParser|Failed to render the CSV file for the following reasons:"
msgstr ""
msgid "CsvParser|Quoted field unterminated"
msgstr ""
msgid "CsvParser|Too few fields"
msgstr ""
msgid "CsvParser|Too many fields"
msgstr ""
msgid "CsvParser|Trailing quote on quoted field is malformed"
msgstr ""
msgid "CsvParser|Unable to auto-detect delimiter; defaulted to \",\""
msgstr ""
msgid "Current" msgid "Current"
msgstr "" msgstr ""
......
import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui'; import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { getAllByRole } from '@testing-library/dom'; import { getAllByRole } from '@testing-library/dom';
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import CSVViewer from '~/blob/csv/csv_viewer.vue'; import CsvViewer from '~/blob/csv/csv_viewer.vue';
import PapaParseAlert from '~/vue_shared/components/papa_parse_alert.vue';
const validCsv = 'one,two,three'; const validCsv = 'one,two,three';
const brokenCsv = '{\n "json": 1,\n "key": [1, 2, 3]\n}'; const brokenCsv = '{\n "json": 1,\n "key": [1, 2, 3]\n}';
...@@ -11,7 +12,7 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => { ...@@ -11,7 +12,7 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
let wrapper; let wrapper;
const createComponent = ({ csv = validCsv, mountFunction = shallowMount } = {}) => { const createComponent = ({ csv = validCsv, mountFunction = shallowMount } = {}) => {
wrapper = mountFunction(CSVViewer, { wrapper = mountFunction(CsvViewer, {
propsData: { propsData: {
csv, csv,
}, },
...@@ -20,7 +21,7 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => { ...@@ -20,7 +21,7 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
const findCsvTable = () => wrapper.findComponent(GlTable); const findCsvTable = () => wrapper.findComponent(GlTable);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(GlAlert); const findAlert = () => wrapper.findComponent(PapaParseAlert);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -35,12 +36,12 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => { ...@@ -35,12 +36,12 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
}); });
describe('when the CSV contains errors', () => { describe('when the CSV contains errors', () => {
it('should render alert', async () => { it('should render alert with correct props', async () => {
createComponent({ csv: brokenCsv }); createComponent({ csv: brokenCsv });
await nextTick; await nextTick;
expect(findAlert().props()).toMatchObject({ expect(findAlert().props()).toMatchObject({
variant: 'danger', papaParseErrors: [{ code: 'UndetectableDelimiter' }],
}); });
}); });
}); });
......
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import PapaParseAlert from '~/vue_shared/components/papa_parse_alert.vue';
describe('app/assets/javascripts/vue_shared/components/papa_parse_alert.vue', () => {
let wrapper;
const createComponent = ({ errorMessages } = {}) => {
wrapper = shallowMount(PapaParseAlert, {
propsData: {
papaParseErrors: errorMessages,
},
});
};
const findAlert = () => wrapper.findComponent(GlAlert);
afterEach(() => {
wrapper.destroy();
});
it('should render alert with correct props', async () => {
createComponent({ errorMessages: [{ code: 'MissingQuotes' }] });
await nextTick;
expect(findAlert().props()).toMatchObject({
variant: 'danger',
});
expect(findAlert().text()).toContain(
'Failed to render the CSV file for the following reasons:',
);
expect(findAlert().text()).toContain('Quoted field unterminated');
});
it('should render original message if no translation available', async () => {
createComponent({
errorMessages: [{ code: 'NotDefined', message: 'Error code is undefined' }],
});
await nextTick;
expect(findAlert().text()).toContain('Error code is undefined');
});
});
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