Commit ec0090a1 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'export-selected-fields' into 'master'

Export only selected fields in requirements

See merge request gitlab-org/gitlab!53707
parents ad3678e3 c6475697
......@@ -262,6 +262,7 @@ For GitLab.com, it is set to 10 MB.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/290813) in GitLab 13.8.
> - Revised CSV column headers [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299247) in GitLab 13.9.
> - Ability to select which fields to export [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/290823) in GitLab 13.9.
You can export GitLab requirements to a
[CSV file](https://en.wikipedia.org/wiki/Comma-separated_values) sent to your default notification
......@@ -276,7 +277,14 @@ Users with Reporter or higher [permissions](../../permissions.md) can export req
To export 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. In the top right, select the **Export as CSV** icon (**{export}**).
A confirmation modal appears.
1. Under **Advanced export options**, select which fields to export.
All fields are selected by default. To exclude a field from being exported, clear the checkbox next to it.
1. Select **Export requirements**. The exported CSV file is sent to the email address associated with your user.
### Exported CSV file format
......
<script>
import { GlModal, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import { GlModal, GlSprintf, GlAlert, GlFormCheckbox } from '@gitlab/ui';
import { uniq } from 'lodash';
import { __, sprintf } from '~/locale';
export default {
i18n: {
modalTitle: __('Export %{requirementsCount} requirements?'),
exportRequirements: __('Export requirements'),
},
components: {
GlModal,
GlSprintf,
GlFormCheckbox,
GlAlert,
},
props: {
email: {
......@@ -20,17 +24,86 @@ export default {
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: {
show() {
this.selectedFields = this.$options.fields.map((f) => f.key);
this.$refs.modal.show();
},
hide() {
this.$refs.modal.hide();
},
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>
......@@ -39,27 +112,50 @@ export default {
ref="modal"
size="sm"
modal-id="export-requirements"
:title="$options.i18n.exportRequirements"
dialog-class="gl-mx-5"
:title="modalTitle"
:ok-title="$options.i18n.exportRequirements"
ok-variant="success"
ok-variant="info"
ok-only
@ok="handleExport"
>
<p>
<gl-sprintf
:message="
__(
'%{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>
<strong>{{ email }}</strong>
</template>
</gl-sprintf>
<gl-alert :dismissible="false">
<gl-sprintf :message="__('These will be sent to %{email} in an attachment once finished.')">
<template #email>
<strong>{{ email }}</strong>
</template>
</gl-sprintf>
</gl-alert>
</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>
</template>
......@@ -408,7 +408,7 @@ export default {
createFlash({ message });
});
},
exportCsv() {
exportCsv(selectedFields) {
return this.$apollo
.mutate({
mutation: exportRequirement,
......@@ -418,6 +418,7 @@ export default {
authorUsername: this.authorUsernames,
search: this.textSearch,
sortBy: this.sortBy,
selectedFields,
},
})
.catch((e) => {
......
......@@ -4,6 +4,7 @@ mutation exportRequirements(
$authorUsername: [String!] = []
$search: String = ""
$sortBy: Sort = CREATED_DESC
$selectedFields: [String!] = []
) {
exportRequirements(
input: {
......@@ -12,6 +13,7 @@ mutation exportRequirements(
authorUsername: $authorUsername
state: $state
sort: $sortBy
selectedFields: $selectedFields
}
) {
errors
......
......@@ -143,3 +143,41 @@
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 { GlModal, GlFormCheckbox } from '@gitlab/ui';
import ExportRequirementsModal from 'ee/requirements/components/export_requirements_modal.vue';
......@@ -32,6 +32,74 @@ describe('ExportRequirementsModal', () => {
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', () => {
......@@ -42,5 +110,17 @@ describe('ExportRequirementsModal', () => {
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 ""
msgid "%{reportType} detected %{totalStart}no%{totalEnd} vulnerabilities."
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}."
msgstr ""
......@@ -2552,6 +2549,9 @@ msgstr ""
msgid "Advanced Settings"
msgstr ""
msgid "Advanced export options"
msgstr ""
msgid "Advanced permissions, Large File Storage and Two-Factor authentication settings."
msgstr ""
......@@ -12125,6 +12125,9 @@ msgstr ""
msgid "Export"
msgstr ""
msgid "Export %{requirementsCount} requirements?"
msgstr ""
msgid "Export as CSV"
msgstr ""
......@@ -22187,6 +22190,9 @@ msgstr ""
msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please select what should be included in each exported requirement."
msgstr ""
msgid "Please set a new password before proceeding."
msgstr ""
......@@ -29805,6 +29811,9 @@ msgstr ""
msgid "These variables are inherited from the parent group."
msgstr ""
msgid "These will be sent to %{email} in an attachment once finished."
msgstr ""
msgid "Third Party Advisory Link"
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