Commit 68475d10 authored by Rajat Jain's avatar Rajat Jain

Export only selected fields in requirements

Pass selectedFields in the graphql mutation to only allow exporting of
fields fields in export requirements
parent c867b405
...@@ -277,8 +277,20 @@ To export requirements: ...@@ -277,8 +277,20 @@ To export requirements:
1. In a project, go to **Requirements**. 1. In a project, go to **Requirements**.
1. Select the **Export as CSV** icon (**{export}**) in the top right. A confirmation modal appears. 1. Select the **Export as CSV** icon (**{export}**) in the top right. A confirmation modal appears.
1. All columns are selected by default. Uncheck any box to exclude it from the exported file.
1. Select **Export requirements**. The exported CSV file is sent to the email address associated with your user. 1. Select **Export requirements**. The exported CSV file is sent to the email address associated with your user.
### Advanced export options
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/290823) in GitLab 13.9.
You can also pick and choose the columns that gets exported in the CSV file using
the advanced export options. By default, all the columns are selected.
You can remove any column from the exported CSV by unselecting the checkbox associated with it.
![export requirements advanced options](img/advanced_export_options_v13_9.png)
### Exported CSV file format ### Exported CSV file format
<!-- vale gitlab.Spelling = NO --> <!-- vale gitlab.Spelling = NO -->
......
<script> <script>
import { GlModal, GlSprintf } from '@gitlab/ui'; import { GlModal, GlSprintf, GlAlert, GlFormCheckbox } from '@gitlab/ui';
import { __ } from '~/locale'; import { uniq } from 'lodash';
import { __, sprintf } from '~/locale';
export default { export default {
i18n: { i18n: {
modalTitle: __('Export %{requirementsCount} requirements?'),
exportRequirements: __('Export requirements'), exportRequirements: __('Export requirements'),
}, },
components: { components: {
GlModal, GlModal,
GlSprintf, GlSprintf,
GlFormCheckbox,
GlAlert,
}, },
props: { props: {
email: { email: {
...@@ -20,17 +24,86 @@ export default { ...@@ -20,17 +24,86 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
selectedFields: this.$options.fields.map((f) => f.key),
};
},
computed: {
modalTitle() {
return sprintf(this.$options.i18n.modalTitle, { requirementsCount: this.requirementCount });
},
selectedOptionsProps() {
const selectedLength = this.selectedFields.length;
const totalLength = this.$options.fields.length;
let checked = false;
if (selectedLength === 0) {
checked = false;
} else if (selectedLength === totalLength) {
checked = true;
}
return {
indeterminate: selectedLength !== 0 && selectedLength !== totalLength,
checked,
};
},
},
methods: { methods: {
show() { show() {
this.selectedFields = this.$options.fields.map((f) => f.key);
this.$refs.modal.show(); this.$refs.modal.show();
}, },
hide() { hide() {
this.$refs.modal.hide(); this.$refs.modal.hide();
}, },
handleExport() { handleExport() {
this.$emit('export'); this.$emit('export', this.selectedFields);
},
toggleField(field) {
const index = this.selectedFields.indexOf(field);
if (index !== -1) {
const tmp = [...this.selectedFields];
tmp.splice(index, 1);
this.selectedFields = tmp;
} else {
this.selectedFields = [...this.selectedFields, field];
}
}, },
isFieldSelected(field) {
return this.selectedFields.includes(field);
}, },
toggleAllFields() {
const { indeterminate, checked } = this.selectedOptionsProps;
if (indeterminate) {
this.selectedFields = uniq([
...this.selectedFields,
...this.$options.fields.map((f) => f.key),
]);
return;
}
if (checked) {
this.selectedFields = [];
} else {
this.selectedFields = [...this.$options.fields.map((f) => f.key)];
}
},
},
/* eslint-disable @gitlab/require-i18n-strings */
fields: [
{ key: 'requirement id', value: 'Requirement ID' },
{ key: 'title', value: 'Title' },
{ key: 'description', value: 'Description' },
{ key: 'author', value: 'Author' },
{ key: 'author username', value: 'Author Username' },
{ key: 'created at (utc)', value: 'Created At (UTC)' },
{ key: 'state', value: 'State' },
{ key: 'state updated at (utc)', value: 'State Updated At (UTC)' },
],
}; };
</script> </script>
...@@ -39,27 +112,50 @@ export default { ...@@ -39,27 +112,50 @@ export default {
ref="modal" ref="modal"
size="sm" size="sm"
modal-id="export-requirements" modal-id="export-requirements"
:title="$options.i18n.exportRequirements" dialog-class="gl-mx-5"
:title="modalTitle"
:ok-title="$options.i18n.exportRequirements" :ok-title="$options.i18n.exportRequirements"
ok-variant="success" ok-variant="info"
ok-only ok-only
@ok="handleExport" @ok="handleExport"
> >
<p> <p>
<gl-sprintf <gl-alert :dismissible="false">
:message=" <gl-sprintf :message="__('These will be sent to %{email} in an attachment once finished.')">
__(
'%{requirementCount} requirements have been selected for export. These will be sent to %{email} as an attachment once finished.',
)
"
>
<template #requirementCount>
<strong>{{ requirementCount }}</strong>
</template>
<template #email> <template #email>
<strong>{{ email }}</strong> <strong>{{ email }}</strong>
</template> </template>
</gl-sprintf> </gl-sprintf>
</gl-alert>
</p> </p>
<div>
<h5 class="gl-mb-0! gl-mt-5">{{ __('Advanced export options') }}</h5>
<p class="gl-mb-3">
{{ __('Please select what should be included in each exported requirement.') }}
</p>
<div class="scrollbox gl-mb-3">
<div class="scrollbox-header gl-p-5">
<gl-form-checkbox
v-bind="selectedOptionsProps"
class="gl-mb-0 gl-mr-0"
@change="toggleAllFields"
>{{ __('Select all') }}</gl-form-checkbox
>
</div>
<div class="gl-pt-5 gl-pb-4 scrollbox-body">
<gl-form-checkbox
v-for="field in $options.fields"
:key="field.key"
class="gl-mb-0 gl-mr-0 form-check gl-pb-5 gl-ml-5"
:checked="isFieldSelected(field.key)"
@change="() => toggleField(field.key)"
>{{ field.value }}</gl-form-checkbox
>
</div>
<div class="scrollbox-fade"></div>
</div>
</div>
</gl-modal> </gl-modal>
</template> </template>
...@@ -408,7 +408,7 @@ export default { ...@@ -408,7 +408,7 @@ export default {
createFlash({ message }); createFlash({ message });
}); });
}, },
exportCsv() { exportCsv(selectedFields) {
return this.$apollo return this.$apollo
.mutate({ .mutate({
mutation: exportRequirement, mutation: exportRequirement,
...@@ -418,6 +418,7 @@ export default { ...@@ -418,6 +418,7 @@ export default {
authorUsername: this.authorUsernames, authorUsername: this.authorUsernames,
search: this.textSearch, search: this.textSearch,
sortBy: this.sortBy, sortBy: this.sortBy,
selectedFields,
}, },
}) })
.catch((e) => { .catch((e) => {
......
...@@ -4,6 +4,7 @@ mutation exportRequirements( ...@@ -4,6 +4,7 @@ mutation exportRequirements(
$authorUsername: [String!] = [] $authorUsername: [String!] = []
$search: String = "" $search: String = ""
$sortBy: Sort = CREATED_DESC $sortBy: Sort = CREATED_DESC
$selectedFields: [String!] = []
) { ) {
exportRequirements( exportRequirements(
input: { input: {
...@@ -12,6 +13,7 @@ mutation exportRequirements( ...@@ -12,6 +13,7 @@ mutation exportRequirements(
authorUsername: $authorUsername authorUsername: $authorUsername
state: $state state: $state
sort: $sortBy sort: $sortBy
selectedFields: $selectedFields
} }
) { ) {
errors errors
......
...@@ -143,3 +143,41 @@ ...@@ -143,3 +143,41 @@
max-width: 100%; max-width: 100%;
} }
} }
#export-requirements {
.scrollbox {
border: 1px solid $gray-200;
border-radius: $border-radius-default;
position: relative;
.scrollbox-header {
border-bottom: 1px solid $gray-200;
}
.scrollbox-body {
max-height: 200px;
overflow: auto;
}
.scrollbox-fade {
position: absolute;
bottom: 0;
height: 20px;
width: 100%;
background-image: linear-gradient(180deg, transparent 0%, $white 100%);
z-index: 1;
border-radius: $border-radius-default;
}
}
.modal-content {
align-self: flex-start;
margin-top: $gl-spacing-scale-11;
}
@media (max-width: $breakpoint-sm) {
.modal-dialog {
margin-top: $gl-spacing-scale-11;
}
}
}
---
title: Export only selected fields in requirements
merge_request: 53707
author:
type: added
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlModal, GlFormCheckbox } from '@gitlab/ui';
import ExportRequirementsModal from 'ee/requirements/components/export_requirements_modal.vue'; import ExportRequirementsModal from 'ee/requirements/components/export_requirements_modal.vue';
...@@ -32,6 +32,74 @@ describe('ExportRequirementsModal', () => { ...@@ -32,6 +32,74 @@ describe('ExportRequirementsModal', () => {
expect(emitted).toBeDefined(); expect(emitted).toBeDefined();
}); });
}); });
describe('toggleField', () => {
it("removes field if it's already selected", async () => {
const [field] = wrapper.vm.$options.fields;
wrapper.vm.toggleField(field.key);
expect(wrapper.vm.selectedFields.includes(field)).toBe(false);
});
it("adds field if it's not selected", async () => {
const [field] = wrapper.vm.$options.fields;
await wrapper.setData({
selectedFields: wrapper.vm.$options.fields.slice(1).map((f) => f.key),
});
wrapper.vm.toggleField(field.key);
expect(wrapper.vm.selectedFields.includes(field.key)).toBe(true);
});
});
describe('isFieldSelected', () => {
it('returns true when field is in selectedFields', () => {
const [field] = wrapper.vm.$options.fields;
expect(wrapper.vm.isFieldSelected(field.key)).toBe(true);
});
it('returns false when field is in selectedFields', async () => {
const [field] = wrapper.vm.$options.fields;
await wrapper.setData({
selectedFields: wrapper.vm.$options.fields.slice(1).map((f) => f.key),
});
expect(wrapper.vm.isFieldSelected(field.key)).toBe(false);
});
});
describe('toggleAllFields', () => {
it('selects all if few are selected', async () => {
await wrapper.setData({
selectedFields: wrapper.vm.$options.fields.slice(1).map((f) => f.key),
});
wrapper.vm.toggleAllFields();
expect(wrapper.vm.selectedFields).toHaveLength(wrapper.vm.$options.fields.length);
});
it('unchecks all if all are selected', () => {
wrapper.vm.toggleAllFields();
expect(wrapper.vm.selectedFields).toHaveLength(0);
});
it('selects all if none are selected', async () => {
await wrapper.setData({
selectedFields: [],
});
wrapper.vm.toggleAllFields();
expect(wrapper.vm.selectedFields).toHaveLength(wrapper.vm.$options.fields.length);
});
});
}); });
describe('template', () => { describe('template', () => {
...@@ -42,5 +110,17 @@ describe('ExportRequirementsModal', () => { ...@@ -42,5 +110,17 @@ describe('ExportRequirementsModal', () => {
expect(emitted).toBeDefined(); expect(emitted).toBeDefined();
}); });
it('renders checkboxes for advanced exporting', () => {
const checkboxes = wrapper.find('.scrollbox-body').findAll(GlFormCheckbox);
expect(checkboxes).toHaveLength(wrapper.vm.$options.fields.length);
});
it('renders Select all checkbox', () => {
const checkbox = wrapper.find('.scrollbox-header').findAll(GlFormCheckbox);
expect(checkbox).toHaveLength(1);
});
}); });
}); });
...@@ -738,9 +738,6 @@ msgstr "" ...@@ -738,9 +738,6 @@ msgstr ""
msgid "%{reportType} detected %{totalStart}no%{totalEnd} vulnerabilities." msgid "%{reportType} detected %{totalStart}no%{totalEnd} vulnerabilities."
msgstr "" msgstr ""
msgid "%{requirementCount} requirements have been selected for export. These will be sent to %{email} as an attachment once finished."
msgstr ""
msgid "%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}." msgid "%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}."
msgstr "" msgstr ""
...@@ -2547,6 +2544,9 @@ msgstr "" ...@@ -2547,6 +2544,9 @@ msgstr ""
msgid "Advanced Settings" msgid "Advanced Settings"
msgstr "" msgstr ""
msgid "Advanced export options"
msgstr ""
msgid "Advanced permissions, Large File Storage and Two-Factor authentication settings." msgid "Advanced permissions, Large File Storage and Two-Factor authentication settings."
msgstr "" msgstr ""
...@@ -12114,6 +12114,9 @@ msgstr "" ...@@ -12114,6 +12114,9 @@ msgstr ""
msgid "Export" msgid "Export"
msgstr "" msgstr ""
msgid "Export %{requirementsCount} requirements?"
msgstr ""
msgid "Export as CSV" msgid "Export as CSV"
msgstr "" msgstr ""
...@@ -22149,6 +22152,9 @@ msgstr "" ...@@ -22149,6 +22152,9 @@ msgstr ""
msgid "Please select at least one filter to see results" msgid "Please select at least one filter to see results"
msgstr "" msgstr ""
msgid "Please select what should be included in each exported requirement."
msgstr ""
msgid "Please set a new password before proceeding." msgid "Please set a new password before proceeding."
msgstr "" msgstr ""
...@@ -29752,6 +29758,9 @@ msgstr "" ...@@ -29752,6 +29758,9 @@ msgstr ""
msgid "These variables are inherited from the parent group." msgid "These variables are inherited from the parent group."
msgstr "" msgstr ""
msgid "These will be sent to %{email} in an attachment once finished."
msgstr ""
msgid "Third Party Advisory Link" msgid "Third Party Advisory Link"
msgstr "" msgstr ""
......
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