Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
ae2d728d
Commit
ae2d728d
authored
Jan 13, 2021
by
Rajat Jain
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Export requirements as a CSV
Export requirements as a CSV
parent
02d36848
Changes
11
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
253 additions
and
11 deletions
+253
-11
ee/app/assets/javascripts/requirements/components/export_requirements_modal.vue
...pts/requirements/components/export_requirements_modal.vue
+65
-0
ee/app/assets/javascripts/requirements/components/requirements_root.vue
...javascripts/requirements/components/requirements_root.vue
+38
-0
ee/app/assets/javascripts/requirements/components/requirements_tabs.vue
...javascripts/requirements/components/requirements_tabs.vue
+22
-10
ee/app/assets/javascripts/requirements/queries/exportRequirements.mutation.graphql
.../requirements/queries/exportRequirements.mutation.graphql
+19
-0
ee/app/assets/javascripts/requirements/requirements_bundle.js
...pp/assets/javascripts/requirements/requirements_bundle.js
+3
-0
ee/app/views/projects/requirements_management/requirements/index.html.haml
...ects/requirements_management/requirements/index.html.haml
+1
-0
ee/changelogs/unreleased/export-requirements.yml
ee/changelogs/unreleased/export-requirements.yml
+5
-0
ee/spec/frontend/requirements/components/export_requirements_modal_spec.js
...requirements/components/export_requirements_modal_spec.js
+47
-0
ee/spec/frontend/requirements/components/requirements_root_spec.js
...rontend/requirements/components/requirements_root_spec.js
+42
-0
ee/spec/frontend/requirements/components/requirements_tabs_spec.js
...rontend/requirements/components/requirements_tabs_spec.js
+2
-1
locale/gitlab.pot
locale/gitlab.pot
+9
-0
No files found.
ee/app/assets/javascripts/requirements/components/export_requirements_modal.vue
0 → 100644
View file @
ae2d728d
<
script
>
import
{
GlModal
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
export
default
{
i18n
:
{
exportRequirements
:
__
(
'
Export requirements
'
),
},
components
:
{
GlModal
,
GlSprintf
,
},
props
:
{
email
:
{
type
:
String
,
required
:
true
,
},
requirementCount
:
{
type
:
Number
,
required
:
true
,
},
},
methods
:
{
show
()
{
this
.
$refs
.
modal
.
show
();
},
hide
()
{
this
.
$refs
.
modal
.
hide
();
},
handleExport
()
{
this
.
$emit
(
'
export
'
);
},
},
};
</
script
>
<
template
>
<gl-modal
ref=
"modal"
size=
"sm"
modal-id=
"export-requirements"
:title=
"$options.i18n.exportRequirements"
:ok-title=
"$options.i18n.exportRequirements"
ok-variant=
"success"
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>
</p>
</gl-modal>
</template>
ee/app/assets/javascripts/requirements/components/requirements_root.vue
View file @
ae2d728d
...
...
@@ -19,11 +19,13 @@ import RequirementsEmptyState from './requirements_empty_state.vue';
import
RequirementItem
from
'
./requirement_item.vue
'
;
import
RequirementForm
from
'
./requirement_form.vue
'
;
import
ImportRequirementsModal
from
'
./import_requirements_modal.vue
'
;
import
ExportRequirementsModal
from
'
./export_requirements_modal.vue
'
;
import
projectRequirements
from
'
../queries/projectRequirements.query.graphql
'
;
import
projectRequirementsCount
from
'
../queries/projectRequirementsCount.query.graphql
'
;
import
createRequirement
from
'
../queries/createRequirement.mutation.graphql
'
;
import
updateRequirement
from
'
../queries/updateRequirement.mutation.graphql
'
;
import
exportRequirement
from
'
../queries/exportRequirements.mutation.graphql
'
;
import
{
FilterState
,
...
...
@@ -45,6 +47,7 @@ export default {
RequirementCreateForm
:
RequirementForm
,
RequirementEditForm
:
RequirementForm
,
ImportRequirementsModal
,
ExportRequirementsModal
,
},
mixins
:
[
glFeatureFlagsMixin
(),
Tracking
.
mixin
()],
props
:
{
...
...
@@ -108,6 +111,10 @@ export default {
type
:
String
,
required
:
true
,
},
currentUserEmail
:
{
type
:
String
,
required
:
true
,
},
},
apollo
:
{
requirements
:
{
...
...
@@ -404,6 +411,26 @@ export default {
createFlash
({
message
});
});
},
exportCsv
()
{
return
this
.
$apollo
.
mutate
({
mutation
:
exportRequirement
,
variables
:
{
projectPath
:
this
.
projectPath
,
state
:
this
.
filterBy
,
authorUsername
:
this
.
authorUsernames
,
search
:
this
.
textSearch
,
sortBy
:
this
.
sortBy
,
},
})
.
catch
((
e
)
=>
{
createFlash
({
message
:
__
(
'
Something went wrong while exporting requirements
'
),
captureError
:
true
,
});
throw
e
;
});
},
handleTabClick
({
filterBy
})
{
this
.
filterBy
=
filterBy
;
this
.
prevPageCursor
=
''
;
...
...
@@ -597,6 +624,9 @@ export default {
handleImportRequirementsClick
()
{
this
.
$refs
.
modal
.
show
();
},
handleExportRequirementsClick
()
{
this
.
$refs
.
exportModal
.
show
();
},
},
};
</
script
>
...
...
@@ -612,6 +642,7 @@ export default {
@
click-tab=
"handleTabClick"
@
click-new-requirement=
"handleNewRequirementClick"
@
click-import-requirements=
"handleImportRequirementsClick"
@
click-export-requirements=
"handleExportRequirementsClick"
/>
<filtered-search-bar
:namespace=
"projectPath"
...
...
@@ -689,5 +720,12 @@ export default {
:project-path=
"projectPath"
@
import=
"importCsv"
/>
<export-requirements-modal
v-if=
"glFeatures.importRequirementsCsv"
ref=
"exportModal"
:requirement-count=
"totalRequirementsForCurrentTab"
:email=
"currentUserEmail"
@
export=
"exportCsv"
/>
</div>
</
template
>
ee/app/assets/javascripts/requirements/components/requirements_tabs.vue
View file @
ae2d728d
<
script
>
import
{
GlBadge
,
GlButton
,
GlTabs
,
GlTab
}
from
'
@gitlab/ui
'
;
import
{
GlBadge
,
GlButton
,
Gl
ButtonGroup
,
Gl
Tabs
,
GlTab
}
from
'
@gitlab/ui
'
;
import
{
FilterState
}
from
'
../constants
'
;
...
...
@@ -10,6 +10,7 @@ export default {
GlButton
,
GlTabs
,
GlTab
,
GlButtonGroup
,
},
props
:
{
filterBy
:
{
...
...
@@ -85,6 +86,15 @@ export default {
</gl-tab>
</gl-tabs>
<div
v-if=
"isOpenTab && canCreateRequirement"
class=
"nav-controls"
>
<gl-button-group>
<gl-button
v-if=
"showUploadCsv"
category=
"secondary"
variant=
"default"
:disabled=
"showCreateForm"
icon=
"export"
@
click=
"$emit('click-export-requirements')"
/>
<gl-button
v-if=
"showUploadCsv"
category=
"secondary"
...
...
@@ -94,6 +104,8 @@ export default {
icon=
"import"
@
click=
"$emit('click-import-requirements')"
/>
</gl-button-group>
<gl-button
category=
"primary"
variant=
"success"
...
...
ee/app/assets/javascripts/requirements/queries/exportRequirements.mutation.graphql
0 → 100644
View file @
ae2d728d
mutation
exportRequirements
(
$projectPath
:
ID
!
$state
:
RequirementState
$authorUsername
:
[
String
!]
=
[]
$search
:
String
=
""
$sortBy
:
Sort
=
CREATED_DESC
)
{
exportRequirements
(
input
:
{
projectPath
:
$projectPath
search
:
$search
authorUsername
:
$authorUsername
state
:
$state
sort
:
$sortBy
}
)
{
errors
}
}
ee/app/assets/javascripts/requirements/requirements_bundle.js
View file @
ae2d728d
...
...
@@ -59,6 +59,7 @@ export default () => {
canCreateRequirement
,
requirementsWebUrl
,
requirementsImportCsvPath
:
importCsvPath
,
currentUserEmail
,
}
=
el
.
dataset
;
const
stateFilterBy
=
filterBy
?
FilterState
[
filterBy
]
:
FilterState
.
opened
;
...
...
@@ -84,6 +85,7 @@ export default () => {
canCreateRequirement
,
requirementsWebUrl
,
importCsvPath
,
currentUserEmail
,
};
},
render
(
createElement
)
{
...
...
@@ -102,6 +104,7 @@ export default () => {
canCreateRequirement
:
parseBoolean
(
this
.
canCreateRequirement
),
requirementsWebUrl
:
this
.
requirementsWebUrl
,
importCsvPath
:
this
.
importCsvPath
,
currentUserEmail
:
this
.
currentUserEmail
,
},
});
},
...
...
ee/app/views/projects/requirements_management/requirements/index.html.haml
View file @
ae2d728d
...
...
@@ -29,6 +29,7 @@
opened:
requirements_count
[
'opened'
],
archived:
requirements_count
[
'archived'
],
all:
total_requirements
,
current_user_email:
current_user
.
email
,
requirements_web_url:
project_requirements_management_requirements_path
(
@project
),
can_create_requirement:
"#{can?(current_user, :create_requirement, @project)}"
,
description_preview_path:
preview_markdown_path
(
@project
),
...
...
ee/changelogs/unreleased/export-requirements.yml
0 → 100644
View file @
ae2d728d
---
title
:
Export requirements as a CSV
merge_request
:
51434
author
:
type
:
added
ee/spec/frontend/requirements/components/export_requirements_modal_spec.js
0 → 100644
View file @
ae2d728d
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlModal
}
from
'
@gitlab/ui
'
;
import
ExportRequirementsModal
from
'
ee/requirements/components/export_requirements_modal.vue
'
;
const
createComponent
=
({
requirementCount
=
42
,
email
=
'
admin@example.com
'
}
=
{})
=>
shallowMount
(
ExportRequirementsModal
,
{
propsData
:
{
requirementCount
,
email
,
},
});
describe
(
'
ExportRequirementsModal
'
,
()
=>
{
let
wrapper
;
beforeEach
(()
=>
{
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
handleExport
'
,
()
=>
{
it
(
'
emits `export` event
'
,
()
=>
{
wrapper
.
vm
.
handleExport
();
const
emitted
=
wrapper
.
emitted
(
'
export
'
);
expect
(
emitted
).
toBeDefined
();
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
GlModal open click emits export event
'
,
()
=>
{
wrapper
.
find
(
GlModal
).
vm
.
$emit
(
'
ok
'
);
const
emitted
=
wrapper
.
emitted
(
'
export
'
);
expect
(
emitted
).
toBeDefined
();
});
});
});
ee/spec/frontend/requirements/components/requirements_root_spec.js
View file @
ae2d728d
...
...
@@ -9,6 +9,7 @@ import RequirementsTabs from 'ee/requirements/components/requirements_tabs.vue';
import
createRequirement
from
'
ee/requirements/queries/createRequirement.mutation.graphql
'
;
import
updateRequirement
from
'
ee/requirements/queries/updateRequirement.mutation.graphql
'
;
import
exportRequirement
from
'
ee/requirements/queries/exportRequirements.mutation.graphql
'
;
import
{
TEST_HOST
}
from
'
helpers/test_constants
'
;
import
{
mockTracking
,
unmockTracking
}
from
'
helpers/tracking_helper
'
;
...
...
@@ -46,6 +47,7 @@ const createComponent = ({
canCreateRequirement
=
true
,
requirementsWebUrl
=
'
/gitlab-org/gitlab-shell/-/requirements
'
,
importCsvPath
=
'
/gitlab-org/gitlab-shell/-/requirements/import_csv
'
,
currentUserEmail
=
'
admin@example.com
'
,
}
=
{})
=>
shallowMount
(
RequirementsRoot
,
{
propsData
:
{
...
...
@@ -57,6 +59,7 @@ const createComponent = ({
canCreateRequirement
,
requirementsWebUrl
,
importCsvPath
,
currentUserEmail
,
},
mocks
:
{
$apollo
:
{
...
...
@@ -257,6 +260,14 @@ describe('RequirementsRoot', () => {
},
};
const
mockExportRequirementsMutationResult
=
{
data
:
{
exportRequirements
:
{
errors
:
[],
},
},
};
describe
(
'
getFilteredSearchValue
'
,
()
=>
{
it
(
'
returns array containing applied filter search values
'
,
()
=>
{
wrapper
.
setData
({
...
...
@@ -291,6 +302,37 @@ describe('RequirementsRoot', () => {
});
});
describe
(
'
exportCsv
'
,
()
=>
{
it
(
'
calls `$apollo.mutate` with `exportRequirement` mutation and variables
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
,
'
mutate
'
)
.
mockResolvedValue
(
mockExportRequirementsMutationResult
);
wrapper
.
vm
.
exportCsv
();
expect
(
wrapper
.
vm
.
$apollo
.
mutate
).
toHaveBeenCalledWith
(
expect
.
objectContaining
({
mutation
:
exportRequirement
,
variables
:
{
projectPath
:
wrapper
.
vm
.
projectPath
,
state
:
wrapper
.
vm
.
filterBy
,
authorUsername
:
wrapper
.
vm
.
authorUsernames
,
search
:
wrapper
.
vm
.
textSearch
,
sortBy
:
wrapper
.
vm
.
sortBy
,
},
}),
);
});
it
(
'
calls `createFlash` when request fails
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
,
'
mutate
'
).
mockRejectedValue
(
new
Error
({}));
return
wrapper
.
vm
.
exportCsv
().
catch
(()
=>
{
expect
(
createFlash
).
toHaveBeenCalled
();
});
});
});
describe
(
'
updateRequirement
'
,
()
=>
{
it
(
'
calls `$apollo.mutate` with `updateRequirement` mutation and variables containing `projectPath` & `iid`
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
,
'
mutate
'
).
mockResolvedValue
(
mockUpdateMutationResult
);
...
...
ee/spec/frontend/requirements/components/requirements_tabs_spec.js
View file @
ae2d728d
...
...
@@ -72,7 +72,7 @@ describe('RequirementsTabs', () => {
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
const
buttonEl
=
wrapper
.
findAll
(
GlButton
).
at
(
1
);
const
buttonEl
=
wrapper
.
findAll
(
GlButton
).
at
(
2
);
expect
(
buttonEl
.
exists
()).
toBe
(
true
);
expect
(
buttonEl
.
text
()).
toBe
(
'
New requirement
'
);
...
...
@@ -114,6 +114,7 @@ describe('RequirementsTabs', () => {
expect
(
buttonEl
.
at
(
0
).
props
(
'
disabled
'
)).
toBe
(
true
);
expect
(
buttonEl
.
at
(
1
).
props
(
'
disabled
'
)).
toBe
(
true
);
expect
(
buttonEl
.
at
(
2
).
props
(
'
disabled
'
)).
toBe
(
true
);
});
});
});
...
...
locale/gitlab.pot
View file @
ae2d728d
...
...
@@ -724,6 +724,9 @@ 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 ""
...
...
@@ -11700,6 +11703,9 @@ msgstr ""
msgid "Export project"
msgstr ""
msgid "Export requirements"
msgstr ""
msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
msgstr ""
...
...
@@ -26233,6 +26239,9 @@ msgstr ""
msgid "Something went wrong while editing your comment. Please try again."
msgstr ""
msgid "Something went wrong while exporting requirements"
msgstr ""
msgid "Something went wrong while fetching %{listType} list"
msgstr ""
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment