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
d136ebfe
Commit
d136ebfe
authored
Jun 09, 2021
by
Simon Knox
Committed by
Ezekiel Kigbo
Jun 09, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Rename vue-router-incompatible files
parent
ff8fd0c8
Changes
10
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
934 additions
and
249 deletions
+934
-249
ee/app/assets/javascripts/iterations/components/iteration_form_without_vue_router.vue
...erations/components/iteration_form_without_vue_router.vue
+262
-0
ee/app/assets/javascripts/iterations/components/iteration_report.vue
...ts/javascripts/iterations/components/iteration_report.vue
+21
-92
ee/app/assets/javascripts/iterations/components/iteration_report_without_vue_router.vue
...ations/components/iteration_report_without_vue_router.vue
+244
-0
ee/app/assets/javascripts/iterations/index.js
ee/app/assets/javascripts/iterations/index.js
+17
-3
ee/app/assets/javascripts/pages/groups/iteration_cadences/index.js
...sets/javascripts/pages/groups/iteration_cadences/index.js
+2
-2
ee/app/views/groups/iteration_cadences/_js_app.html.haml
ee/app/views/groups/iteration_cadences/_js_app.html.haml
+6
-1
ee/spec/frontend/iterations/components/iteration_form_without_vue_router_spec.js
...ions/components/iteration_form_without_vue_router_spec.js
+1
-1
ee/spec/frontend/iterations/components/iteration_report_spec.js
...c/frontend/iterations/components/iteration_report_spec.js
+64
-149
ee/spec/frontend/iterations/components/iteration_report_without_vue_router_spec.js
...ns/components/iteration_report_without_vue_router_spec.js
+316
-0
ee/spec/frontend/iterations/mock_data.js
ee/spec/frontend/iterations/mock_data.js
+1
-1
No files found.
ee/app/assets/javascripts/iterations/components/iteration_form_without_vue_router.vue
0 → 100644
View file @
d136ebfe
<
script
>
import
{
GlButton
,
GlForm
,
GlFormInput
}
from
'
@gitlab/ui
'
;
import
initDatePicker
from
'
~/behaviors/date_picker
'
;
import
createFlash
from
'
~/flash
'
;
import
{
visitUrl
}
from
'
~/lib/utils/url_utility
'
;
import
{
__
}
from
'
~/locale
'
;
import
MarkdownField
from
'
~/vue_shared/components/markdown/field.vue
'
;
import
createIteration
from
'
../queries/create_iteration.mutation.graphql
'
;
import
updateIteration
from
'
../queries/update_iteration.mutation.graphql
'
;
export
default
{
components
:
{
GlButton
,
GlForm
,
GlFormInput
,
MarkdownField
,
},
props
:
{
groupPath
:
{
type
:
String
,
required
:
true
,
},
previewMarkdownPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
iterationsListPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
isEditing
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
iteration
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
},
data
()
{
return
{
iterations
:
[],
loading
:
false
,
title
:
this
.
iteration
.
title
,
description
:
this
.
iteration
.
description
,
startDate
:
this
.
iteration
.
startDate
,
dueDate
:
this
.
iteration
.
dueDate
,
};
},
computed
:
{
variables
()
{
return
{
input
:
{
groupPath
:
this
.
groupPath
,
title
:
this
.
title
,
description
:
this
.
description
,
startDate
:
this
.
startDate
,
dueDate
:
this
.
dueDate
,
},
};
},
},
mounted
()
{
// TODO: utilize GlDatepicker instead of relying on this jQuery behavior
initDatePicker
();
},
methods
:
{
save
()
{
this
.
loading
=
true
;
return
this
.
isEditing
?
this
.
updateIteration
()
:
this
.
createIteration
();
},
cancel
()
{
if
(
this
.
iterationsListPath
)
{
visitUrl
(
this
.
iterationsListPath
);
}
else
{
this
.
$emit
(
'
cancel
'
);
}
},
createIteration
()
{
return
this
.
$apollo
.
mutate
({
mutation
:
createIteration
,
variables
:
this
.
variables
,
})
.
then
(({
data
})
=>
{
const
{
errors
,
iteration
}
=
data
.
createIteration
;
if
(
errors
.
length
>
0
)
{
this
.
loading
=
false
;
createFlash
({
message
:
errors
[
0
],
});
return
;
}
visitUrl
(
iteration
.
webUrl
);
})
.
catch
(()
=>
{
this
.
loading
=
false
;
createFlash
({
message
:
__
(
'
Unable to save iteration. Please try again
'
),
});
});
},
updateIteration
()
{
return
this
.
$apollo
.
mutate
({
mutation
:
updateIteration
,
variables
:
{
input
:
{
...
this
.
variables
.
input
,
id
:
this
.
iteration
.
id
,
},
},
})
.
then
(({
data
})
=>
{
const
{
errors
}
=
data
.
updateIteration
;
if
(
errors
.
length
>
0
)
{
createFlash
({
message
:
errors
[
0
],
});
return
;
}
this
.
$emit
(
'
updated
'
);
})
.
catch
(()
=>
{
createFlash
({
message
:
__
(
'
Unable to save iteration. Please try again
'
),
});
})
.
finally
(()
=>
{
this
.
loading
=
false
;
});
},
updateDueDate
(
val
)
{
this
.
dueDate
=
val
;
},
updateStartDate
(
val
)
{
this
.
startDate
=
val
;
},
},
};
</
script
>
<
template
>
<div>
<div
class=
"gl-display-flex"
>
<h3
ref=
"pageTitle"
class=
"page-title"
>
{{
isEditing
?
__
(
'
Edit iteration
'
)
:
__
(
'
New iteration
'
)
}}
</h3>
</div>
<hr
class=
"gl-mt-0"
/>
<gl-form
class=
"row common-note-form"
>
<div
class=
"col-md-6"
>
<div
class=
"form-group row"
>
<div
class=
"col-form-label col-sm-2"
>
<label
for=
"iteration-title"
>
{{
__
(
'
Title
'
)
}}
</label>
</div>
<div
class=
"col-sm-10"
>
<gl-form-input
id=
"iteration-title"
v-model=
"title"
autocomplete=
"off"
data-qa-selector=
"iteration_title_field"
/>
</div>
</div>
<div
class=
"form-group row"
>
<div
class=
"col-form-label col-sm-2"
>
<label
for=
"iteration-description"
>
{{
__
(
'
Description
'
)
}}
</label>
</div>
<div
class=
"col-sm-10"
>
<markdown-field
:markdown-preview-path=
"previewMarkdownPath"
:can-attach-file=
"false"
:enable-autocomplete=
"true"
label=
"Description"
:textarea-value=
"description"
markdown-docs-path=
"/help/user/markdown"
:add-spacing-classes=
"false"
class=
"md-area"
>
<template
#textarea
>
<textarea
id=
"iteration-description"
v-model=
"description"
class=
"note-textarea js-gfm-input js-autosize markdown-area"
dir=
"auto"
data-supports-quick-actions=
"false"
:aria-label=
"__('Description')"
data-qa-selector=
"iteration_description_field"
>
</textarea>
</
template
>
</markdown-field>
</div>
</div>
</div>
<div
class=
"col-md-6"
>
<div
class=
"form-group row"
>
<div
class=
"col-form-label col-sm-2"
>
<label
for=
"iteration-start-date"
>
{{ __('Start date') }}
</label>
</div>
<div
class=
"col-sm-10"
>
<gl-form-input
id=
"iteration-start-date"
v-model=
"startDate"
class=
"datepicker form-control"
:placeholder=
"__('Select start date')"
autocomplete=
"off"
data-qa-selector=
"iteration_start_date_field"
@
change=
"updateStartDate"
/>
<a
class=
"inline float-right gl-mt-2 js-clear-start-date"
href=
"#"
>
{{
__('Clear start date')
}}
</a>
</div>
</div>
<div
class=
"form-group row"
>
<div
class=
"col-form-label col-sm-2"
>
<label
for=
"iteration-due-date"
>
{{ __('Due date') }}
</label>
</div>
<div
class=
"col-sm-10"
>
<gl-form-input
id=
"iteration-due-date"
v-model=
"dueDate"
class=
"datepicker form-control"
:placeholder=
"__('Select due date')"
autocomplete=
"off"
data-qa-selector=
"iteration_due_date_field"
@
change=
"updateDueDate"
/>
<a
class=
"inline float-right gl-mt-2 js-clear-due-date"
href=
"#"
>
{{
__('Clear due date')
}}
</a>
</div>
</div>
</div>
</gl-form>
<div
class=
"form-actions d-flex"
>
<gl-button
:loading=
"loading"
data-testid=
"save-iteration"
variant=
"success"
data-qa-selector=
"save_iteration_button"
@
click=
"save"
>
{{ isEditing ? __('Update iteration') : __('Create iteration') }}
</gl-button>
<gl-button
class=
"ml-auto"
data-testid=
"cancel-iteration"
@
click=
"cancel"
>
{{ __('Cancel') }}
</gl-button>
</div>
</div>
</template>
ee/app/assets/javascripts/iterations/components/iteration_report.vue
View file @
d136ebfe
...
...
@@ -16,7 +16,6 @@ import { __ } from '~/locale';
import
glFeatureFlagsMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
{
Namespace
}
from
'
../constants
'
;
import
query
from
'
../queries/iteration.query.graphql
'
;
import
IterationForm
from
'
./iteration_form.vue
'
;
import
IterationReportTabs
from
'
./iteration_report_tabs.vue
'
;
const
iterationStates
=
{
...
...
@@ -25,11 +24,6 @@ const iterationStates = {
expired
:
'
expired
'
,
};
const
page
=
{
view
:
'
viewIteration
'
,
edit
:
'
editIteration
'
,
};
export
default
{
components
:
{
BurnCharts
,
...
...
@@ -40,7 +34,6 @@ export default {
GlDropdownItem
,
GlEmptyState
,
GlLoadingIcon
,
IterationForm
,
IterationReportTabs
,
},
apollo
:
{
...
...
@@ -64,64 +57,30 @@ export default {
},
},
mixins
:
[
glFeatureFlagsMixin
()],
inject
:
[
'
fullPath
'
],
props
:
{
hasScopedLabelsFeature
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
iterationId
:
{
type
:
String
,
required
:
false
,
default
:
undefined
,
},
canEdit
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
initiallyEditing
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
labelsFetchPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
namespaceType
:
{
type
:
String
,
required
:
false
,
default
:
Namespace
.
Group
,
validator
:
(
value
)
=>
Object
.
values
(
Namespace
).
includes
(
value
),
},
previewMarkdownPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
svgPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
inject
:
[
'
fullPath
'
,
'
hasScopedLabelsFeature
'
,
'
canEditIteration
'
,
'
namespaceType
'
,
'
noIssuesSvgPath
'
,
'
labelsFetchPath
'
,
],
data
()
{
return
{
isEditing
:
this
.
initiallyEditing
,
error
:
''
,
iteration
:
{},
};
},
computed
:
{
canEdit
Iteration
()
{
return
this
.
canEdit
&&
this
.
namespaceType
===
Namespace
.
Group
;
canEdit
()
{
return
this
.
canEdit
Iteration
&&
this
.
namespaceType
===
Namespace
.
Group
;
},
loading
()
{
return
this
.
$apollo
.
queries
.
iteration
.
loading
;
},
iterationId
()
{
return
this
.
$router
.
currentRoute
.
params
.
iterationId
;
},
showEmptyState
()
{
return
!
this
.
loading
&&
this
.
iteration
&&
!
this
.
iteration
.
title
;
},
...
...
@@ -140,37 +99,16 @@ export default {
return
{
text
:
__
(
'
Open
'
),
variant
:
'
success
'
};
}
},
},
mounted
()
{
this
.
boundOnPopState
=
this
.
onPopState
.
bind
(
this
);
window
.
addEventListener
(
'
popstate
'
,
this
.
boundOnPopState
);
},
beforeDestroy
()
{
window
.
removeEventListener
(
'
popstate
'
,
this
.
boundOnPopState
);
editPage
()
{
return
{
name
:
'
editIteration
'
,
};
},
},
methods
:
{
onPopState
(
e
)
{
if
(
e
.
state
?.
prev
===
page
.
view
)
{
this
.
isEditing
=
true
;
}
else
if
(
e
.
state
?.
prev
===
page
.
edit
)
{
this
.
isEditing
=
false
;
}
else
{
this
.
isEditing
=
this
.
initiallyEditing
;
}
},
formatDate
(
date
)
{
return
formatDate
(
date
,
'
mmm d, yyyy
'
,
true
);
},
loadEditPage
()
{
this
.
isEditing
=
true
;
const
newUrl
=
window
.
location
.
pathname
.
replace
(
/
(\/
edit
)?\/?
$/
,
'
/edit
'
);
window
.
history
.
pushState
({
prev
:
page
.
view
},
null
,
newUrl
);
},
loadReportPage
()
{
this
.
isEditing
=
false
;
const
newUrl
=
window
.
location
.
pathname
.
replace
(
/
\/
edit$/
,
''
);
window
.
history
.
pushState
({
prev
:
page
.
edit
},
null
,
newUrl
);
},
},
};
</
script
>
...
...
@@ -186,15 +124,6 @@ export default {
:title=
"__('Could not find iteration')"
:compact=
"false"
/>
<iteration-form
v-else-if=
"isEditing"
:group-path=
"fullPath"
:preview-markdown-path=
"previewMarkdownPath"
:is-editing=
"true"
:iteration=
"iteration"
@
updated=
"loadReportPage"
@
cancel=
"loadReportPage"
/>
<template
v-else
>
<div
ref=
"topbar"
...
...
@@ -207,7 +136,7 @@ export default {
>
{{
formatDate
(
iteration
.
startDate
)
}}
–
{{
formatDate
(
iteration
.
dueDate
)
}}
</span
>
<gl-dropdown
v-if=
"canEdit
Iteration
"
v-if=
"canEdit"
data-testid=
"actions-dropdown"
variant=
"default"
toggle-class=
"gl-text-decoration-none gl-border-0! gl-shadow-none!"
...
...
@@ -218,7 +147,7 @@ export default {
<template
#button-content
>
<gl-icon
name=
"ellipsis_v"
/><span
class=
"gl-sr-only"
>
{{
__
(
'
Actions
'
)
}}
</span>
</
template
>
<gl-dropdown-item
@
click=
"loadE
ditPage"
>
{{ __('Edit iteration') }}
</gl-dropdown-item>
<gl-dropdown-item
:to=
"e
ditPage"
>
{{ __('Edit iteration') }}
</gl-dropdown-item>
</gl-dropdown>
</div>
<h3
ref=
"title"
class=
"page-title"
>
{{ iteration.title }}
</h3>
...
...
@@ -237,7 +166,7 @@ export default {
:iteration-id=
"iteration.id"
:labels-fetch-path=
"labelsFetchPath"
:namespace-type=
"namespaceType"
:svg-path=
"
s
vgPath"
:svg-path=
"
noIssuesS
vgPath"
/>
</template>
</div>
...
...
ee/app/assets/javascripts/iterations/components/iteration_report_without_vue_router.vue
0 → 100644
View file @
d136ebfe
<
script
>
/* eslint-disable vue/no-v-html */
import
{
GlAlert
,
GlBadge
,
GlDropdown
,
GlDropdownItem
,
GlEmptyState
,
GlIcon
,
GlLoadingIcon
,
}
from
'
@gitlab/ui
'
;
import
BurnCharts
from
'
ee/burndown_chart/components/burn_charts.vue
'
;
import
{
convertToGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
formatDate
}
from
'
~/lib/utils/datetime_utility
'
;
import
{
__
}
from
'
~/locale
'
;
import
glFeatureFlagsMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
{
Namespace
}
from
'
../constants
'
;
import
query
from
'
../queries/iteration.query.graphql
'
;
import
IterationForm
from
'
./iteration_form_without_vue_router.vue
'
;
import
IterationReportTabs
from
'
./iteration_report_tabs.vue
'
;
const
iterationStates
=
{
closed
:
'
closed
'
,
upcoming
:
'
upcoming
'
,
expired
:
'
expired
'
,
};
const
page
=
{
view
:
'
viewIteration
'
,
edit
:
'
editIteration
'
,
};
export
default
{
components
:
{
BurnCharts
,
GlAlert
,
GlBadge
,
GlIcon
,
GlDropdown
,
GlDropdownItem
,
GlEmptyState
,
GlLoadingIcon
,
IterationForm
,
IterationReportTabs
,
},
apollo
:
{
iteration
:
{
query
,
/* eslint-disable @gitlab/require-i18n-strings */
variables
()
{
return
{
fullPath
:
this
.
fullPath
,
id
:
convertToGraphQLId
(
'
Iteration
'
,
this
.
iterationId
),
isGroup
:
this
.
namespaceType
===
Namespace
.
Group
,
};
},
/* eslint-enable @gitlab/require-i18n-strings */
update
(
data
)
{
return
data
[
this
.
namespaceType
]?.
iterations
?.
nodes
[
0
]
||
{};
},
error
(
err
)
{
this
.
error
=
err
.
message
;
},
},
},
mixins
:
[
glFeatureFlagsMixin
()],
inject
:
[
'
fullPath
'
],
props
:
{
hasScopedLabelsFeature
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
iterationId
:
{
type
:
String
,
required
:
false
,
default
:
undefined
,
},
canEdit
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
initiallyEditing
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
labelsFetchPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
namespaceType
:
{
type
:
String
,
required
:
false
,
default
:
Namespace
.
Group
,
validator
:
(
value
)
=>
Object
.
values
(
Namespace
).
includes
(
value
),
},
previewMarkdownPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
svgPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
data
()
{
return
{
isEditing
:
this
.
initiallyEditing
,
error
:
''
,
iteration
:
{},
};
},
computed
:
{
canEditIteration
()
{
return
this
.
canEdit
&&
this
.
namespaceType
===
Namespace
.
Group
;
},
loading
()
{
return
this
.
$apollo
.
queries
.
iteration
.
loading
;
},
showEmptyState
()
{
return
!
this
.
loading
&&
this
.
iteration
&&
!
this
.
iteration
.
title
;
},
status
()
{
switch
(
this
.
iteration
.
state
)
{
case
iterationStates
.
closed
:
return
{
text
:
__
(
'
Closed
'
),
variant
:
'
danger
'
,
};
case
iterationStates
.
expired
:
return
{
text
:
__
(
'
Past due
'
),
variant
:
'
warning
'
};
case
iterationStates
.
upcoming
:
return
{
text
:
__
(
'
Upcoming
'
),
variant
:
'
neutral
'
};
default
:
return
{
text
:
__
(
'
Open
'
),
variant
:
'
success
'
};
}
},
},
mounted
()
{
this
.
boundOnPopState
=
this
.
onPopState
.
bind
(
this
);
window
.
addEventListener
(
'
popstate
'
,
this
.
boundOnPopState
);
},
beforeDestroy
()
{
window
.
removeEventListener
(
'
popstate
'
,
this
.
boundOnPopState
);
},
methods
:
{
onPopState
(
e
)
{
if
(
e
.
state
?.
prev
===
page
.
view
)
{
this
.
isEditing
=
true
;
}
else
if
(
e
.
state
?.
prev
===
page
.
edit
)
{
this
.
isEditing
=
false
;
}
else
{
this
.
isEditing
=
this
.
initiallyEditing
;
}
},
formatDate
(
date
)
{
return
formatDate
(
date
,
'
mmm d, yyyy
'
,
true
);
},
loadEditPage
()
{
this
.
isEditing
=
true
;
const
newUrl
=
window
.
location
.
pathname
.
replace
(
/
(\/
edit
)?\/?
$/
,
'
/edit
'
);
window
.
history
.
pushState
({
prev
:
page
.
view
},
null
,
newUrl
);
},
loadReportPage
()
{
this
.
isEditing
=
false
;
const
newUrl
=
window
.
location
.
pathname
.
replace
(
/
\/
edit$/
,
''
);
window
.
history
.
pushState
({
prev
:
page
.
edit
},
null
,
newUrl
);
},
},
};
</
script
>
<
template
>
<div>
<gl-alert
v-if=
"error"
variant=
"danger"
@
dismiss=
"error = ''"
>
{{
error
}}
</gl-alert>
<gl-loading-icon
v-else-if=
"loading"
class=
"gl-py-5"
size=
"lg"
/>
<gl-empty-state
v-else-if=
"showEmptyState"
:title=
"__('Could not find iteration')"
:compact=
"false"
/>
<iteration-form
v-else-if=
"isEditing"
:group-path=
"fullPath"
:preview-markdown-path=
"previewMarkdownPath"
:is-editing=
"true"
:iteration=
"iteration"
@
updated=
"loadReportPage"
@
cancel=
"loadReportPage"
/>
<template
v-else
>
<div
ref=
"topbar"
class=
"gl-display-flex gl-justify-items-center gl-align-items-center gl-py-3 gl-border-1 gl-border-b-solid gl-border-gray-100"
>
<gl-badge
:variant=
"status.variant"
>
{{
status
.
text
}}
</gl-badge>
<span
class=
"gl-ml-4"
>
{{
formatDate
(
iteration
.
startDate
)
}}
–
{{
formatDate
(
iteration
.
dueDate
)
}}
</span
>
<gl-dropdown
v-if=
"canEditIteration"
data-testid=
"actions-dropdown"
variant=
"default"
toggle-class=
"gl-text-decoration-none gl-border-0! gl-shadow-none!"
class=
"gl-ml-auto gl-text-secondary"
right
no-caret
>
<template
#button-content
>
<gl-icon
name=
"ellipsis_v"
/><span
class=
"gl-sr-only"
>
{{
__
(
'
Actions
'
)
}}
</span>
</
template
>
<gl-dropdown-item
@
click=
"loadEditPage"
>
{{ __('Edit iteration') }}
</gl-dropdown-item>
</gl-dropdown>
</div>
<h3
ref=
"title"
class=
"page-title"
>
{{ iteration.title }}
</h3>
<div
ref=
"description"
v-html=
"iteration.descriptionHtml"
></div>
<burn-charts
:start-date=
"iteration.startDate"
:due-date=
"iteration.dueDate"
:iteration-id=
"iteration.id"
:iteration-state=
"iteration.state"
:full-path=
"fullPath"
:namespace-type=
"namespaceType"
/>
<iteration-report-tabs
:full-path=
"fullPath"
:has-scoped-labels-feature=
"hasScopedLabelsFeature"
:iteration-id=
"iteration.id"
:labels-fetch-path=
"labelsFetchPath"
:namespace-type=
"namespaceType"
:svg-path=
"svgPath"
/>
</template>
</div>
</template>
ee/app/assets/javascripts/iterations/index.js
View file @
d136ebfe
...
...
@@ -3,9 +3,10 @@ import VueApollo from 'vue-apollo';
import
createDefaultClient
from
'
~/lib/graphql
'
;
import
{
parseBoolean
}
from
'
~/lib/utils/common_utils
'
;
import
App
from
'
./components/app.vue
'
;
import
IterationForm
from
'
./components/iteration_form.vue
'
;
import
IterationReport
from
'
./components/iteration_report.vue
'
;
import
IterationForm
from
'
./components/iteration_form
_without_vue_router
.vue
'
;
import
IterationReport
from
'
./components/iteration_report
_without_vue_router
.vue
'
;
import
Iterations
from
'
./components/iterations.vue
'
;
import
{
Namespace
}
from
'
./constants
'
;
import
createRouter
from
'
./router
'
;
Vue
.
use
(
VueApollo
);
...
...
@@ -94,7 +95,7 @@ export function initIterationReport({ namespaceType, initiallyEditing } = {}) {
});
}
export
function
initCadenceApp
()
{
export
function
initCadenceApp
(
{
namespaceType
}
)
{
const
el
=
document
.
querySelector
(
'
.js-iteration-cadence-app
'
);
if
(
!
el
)
{
...
...
@@ -106,6 +107,11 @@ export function initCadenceApp() {
cadencesListPath
,
canCreateCadence
,
canEditCadence
,
canEditIteration
,
hasScopedLabelsFeature
,
labelsFetchPath
,
previewMarkdownPath
,
noIssuesSvgPath
,
}
=
el
.
dataset
;
const
router
=
createRouter
(
cadencesListPath
);
...
...
@@ -119,9 +125,17 @@ export function initCadenceApp() {
cadencesListPath
,
canCreateCadence
:
parseBoolean
(
canCreateCadence
),
canEditCadence
:
parseBoolean
(
canEditCadence
),
namespaceType
,
canEditIteration
:
parseBoolean
(
canEditIteration
),
hasScopedLabelsFeature
:
parseBoolean
(
hasScopedLabelsFeature
),
labelsFetchPath
,
previewMarkdownPath
,
noIssuesSvgPath
,
},
render
(
createElement
)
{
return
createElement
(
App
);
},
});
}
export
const
initGroupCadenceApp
=
()
=>
initCadenceApp
({
namespaceType
:
Namespace
.
Group
});
ee/app/assets/javascripts/pages/groups/iteration_cadences/index.js
View file @
d136ebfe
import
{
initCadenceApp
}
from
'
ee/iterations
'
;
import
{
init
Group
CadenceApp
}
from
'
ee/iterations
'
;
initCadenceApp
();
init
Group
CadenceApp
();
ee/app/views/groups/iteration_cadences/_js_app.html.haml
View file @
d136ebfe
.js-iteration-cadence-app
{
data:
{
group_full_path:
@group
.
full_path
,
cadences_list_path:
group_iteration_cadences_path
(
@group
),
can_create_cadence:
can?
(
current_user
,
:create_iteration_cadence
,
@group
).
to_s
,
can_edit_cadence:
can?
(
current_user
,
:admin_iteration_cadence
,
@group
).
to_s
}
}
can_edit_cadence:
can?
(
current_user
,
:admin_iteration_cadence
,
@group
).
to_s
,
can_edit_iteration:
can?
(
current_user
,
:admin_iteration
,
@group
).
to_s
,
has_scoped_labels_feature:
@group
.
licensed_feature_available?
(
:scoped_labels
).
to_s
,
labels_fetch_path:
group_labels_path
(
@group
,
format: :json
,
include_ancestor_groups:
true
),
preview_markdown_path:
preview_markdown_path
(
@group
),
no_issues_svg_path:
image_path
(
'illustrations/issues.svg'
)
}
}
ee/spec/frontend/iterations/components/iteration_form_spec.js
→
ee/spec/frontend/iterations/components/iteration_form_
without_vue_router_
spec.js
View file @
d136ebfe
import
{
GlForm
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
ApolloMutation
}
from
'
vue-apollo
'
;
import
IterationForm
from
'
ee/iterations/components/iteration_form.vue
'
;
import
IterationForm
from
'
ee/iterations/components/iteration_form
_without_vue_router
.vue
'
;
import
createIteration
from
'
ee/iterations/queries/create_iteration.mutation.graphql
'
;
import
updateIteration
from
'
ee/iterations/queries/update_iteration.mutation.graphql
'
;
import
{
TEST_HOST
}
from
'
helpers/test_constants
'
;
...
...
ee/spec/frontend/iterations/components/iteration_report_spec.js
View file @
d136ebfe
This diff is collapsed.
Click to expand it.
ee/spec/frontend/iterations/components/iteration_report_without_vue_router_spec.js
0 → 100644
View file @
d136ebfe
import
{
GlDropdown
,
GlDropdownItem
,
GlEmptyState
,
GlLoadingIcon
,
GlTab
,
GlTabs
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
IterationForm
from
'
ee/iterations/components/iteration_form_without_vue_router.vue
'
;
import
IterationReportTabs
from
'
ee/iterations/components/iteration_report_tabs.vue
'
;
import
IterationReport
from
'
ee/iterations/components/iteration_report_without_vue_router.vue
'
;
import
{
Namespace
}
from
'
ee/iterations/constants
'
;
import
query
from
'
ee/iterations/queries/iteration.query.graphql
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
mockIterationNode
,
mockGroupIterations
,
mockProjectIterations
}
from
'
../mock_data
'
;
const
localVue
=
createLocalVue
();
describe
(
'
Iterations report
'
,
()
=>
{
let
wrapper
;
let
mockApollo
;
const
defaultProps
=
{
fullPath
:
'
gitlab-org
'
,
labelsFetchPath
:
'
/gitlab-org/gitlab-test/-/labels.json?include_ancestor_groups=true
'
,
};
const
findTopbar
=
()
=>
wrapper
.
find
({
ref
:
'
topbar
'
});
const
findTitle
=
()
=>
wrapper
.
find
({
ref
:
'
title
'
});
const
findDescription
=
()
=>
wrapper
.
find
({
ref
:
'
description
'
});
const
findActionsDropdown
=
()
=>
wrapper
.
find
(
'
[data-testid="actions-dropdown"]
'
);
const
clickEditButton
=
()
=>
{
findActionsDropdown
().
vm
.
$emit
(
'
click
'
);
wrapper
.
findComponent
(
GlDropdownItem
).
vm
.
$emit
(
'
click
'
);
};
const
findLoadingIcon
=
()
=>
wrapper
.
findComponent
(
GlLoadingIcon
);
const
findEmptyState
=
()
=>
wrapper
.
findComponent
(
GlEmptyState
);
const
findIterationForm
=
()
=>
wrapper
.
findComponent
(
IterationForm
);
const
mountComponentWithApollo
=
({
props
=
defaultProps
,
iterationQueryHandler
=
jest
.
fn
(),
}
=
{})
=>
{
localVue
.
use
(
VueApollo
);
mockApollo
=
createMockApollo
([[
query
,
iterationQueryHandler
]]);
wrapper
=
shallowMount
(
IterationReport
,
{
localVue
,
apolloProvider
:
mockApollo
,
propsData
:
props
,
provide
:
{
fullPath
:
props
.
fullPath
,
},
stubs
:
{
GlLoadingIcon
,
GlTab
,
GlTabs
,
},
});
};
describe
(
'
with mock apollo
'
,
()
=>
{
describe
.
each
([
[
'
group
'
,
{
fullPath
:
'
group-name
'
,
iterationId
:
String
(
getIdFromGraphQLId
(
mockIterationNode
.
id
)),
},
mockGroupIterations
,
{
fullPath
:
'
group-name
'
,
id
:
mockIterationNode
.
id
,
isGroup
:
true
,
},
],
[
'
project
'
,
{
fullPath
:
'
group-name/project-name
'
,
iterationId
:
String
(
getIdFromGraphQLId
(
mockIterationNode
.
id
)),
namespaceType
:
Namespace
.
Project
,
},
mockProjectIterations
,
{
fullPath
:
'
group-name/project-name
'
,
id
:
mockIterationNode
.
id
,
isGroup
:
false
,
},
],
])(
'
when viewing an iteration in a %s
'
,
(
_
,
props
,
mockIteration
,
expectedParams
)
=>
{
it
(
'
calls a query with correct parameters
'
,
()
=>
{
const
iterationQueryHandler
=
jest
.
fn
();
mountComponentWithApollo
({
props
,
iterationQueryHandler
,
});
expect
(
iterationQueryHandler
).
toHaveBeenNthCalledWith
(
1
,
expectedParams
);
});
it
(
'
renders an iteration title
'
,
async
()
=>
{
mountComponentWithApollo
({
props
,
iterationQueryHandler
:
jest
.
fn
().
mockResolvedValue
(
mockIteration
),
});
await
waitForPromises
();
expect
(
findTitle
().
text
()).
toContain
(
mockIterationNode
.
title
);
});
});
});
const
mountComponent
=
({
props
=
defaultProps
,
loading
=
false
}
=
{})
=>
{
wrapper
=
shallowMount
(
IterationReport
,
{
propsData
:
props
,
mocks
:
{
$apollo
:
{
queries
:
{
iteration
:
{
loading
}
},
},
},
provide
:
{
fullPath
:
props
.
fullPath
,
},
stubs
:
{
GlLoadingIcon
,
GlTab
,
GlTabs
,
},
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
it
(
'
shows spinner while loading
'
,
()
=>
{
mountComponent
({
loading
:
true
,
});
expect
(
findLoadingIcon
().
exists
()).
toBe
(
true
);
});
describe
(
'
empty state
'
,
()
=>
{
it
(
'
shows empty state if no item loaded
'
,
()
=>
{
mountComponent
({
loading
:
false
,
});
expect
(
findEmptyState
().
props
(
'
title
'
)).
toBe
(
'
Could not find iteration
'
);
expect
(
findTitle
().
exists
()).
toBe
(
false
);
expect
(
findDescription
().
exists
()).
toBe
(
false
);
expect
(
findActionsDropdown
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
item loaded
'
,
()
=>
{
const
iteration
=
{
title
:
'
June week 1
'
,
id
:
'
gid://gitlab/Iteration/2
'
,
descriptionHtml
:
'
The first week of June
'
,
startDate
:
'
2020-06-02
'
,
dueDate
:
'
2020-06-08
'
,
state
:
'
opened
'
,
};
describe
(
'
user without edit permission
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
({
loading
:
false
,
});
wrapper
.
setData
({
iteration
,
});
});
it
(
'
shows status and date in header
'
,
()
=>
{
expect
(
findTopbar
().
text
()).
toContain
(
'
Open
'
);
expect
(
findTopbar
().
text
()).
toContain
(
'
Jun 2, 2020
'
);
expect
(
findTopbar
().
text
()).
toContain
(
'
Jun 8, 2020
'
);
});
it
(
'
hides empty region and loading spinner
'
,
()
=>
{
expect
(
findLoadingIcon
().
exists
()).
toBe
(
false
);
expect
(
findEmptyState
().
exists
()).
toBe
(
false
);
});
it
(
'
shows title and description
'
,
()
=>
{
expect
(
findTitle
().
text
()).
toContain
(
iteration
.
title
);
expect
(
findDescription
().
text
()).
toContain
(
iteration
.
descriptionHtml
);
});
it
(
'
hides actions dropdown
'
,
()
=>
{
expect
(
findActionsDropdown
().
exists
()).
toBe
(
false
);
});
it
(
'
shows IterationReportTabs component
'
,
()
=>
{
const
iterationReportTabs
=
wrapper
.
findComponent
(
IterationReportTabs
);
expect
(
iterationReportTabs
.
props
()).
toMatchObject
({
fullPath
:
defaultProps
.
fullPath
,
iterationId
:
iteration
.
id
,
labelsFetchPath
:
defaultProps
.
labelsFetchPath
,
namespaceType
:
Namespace
.
Group
,
});
});
});
describe
(
'
user with edit permission
'
,
()
=>
{
describe
(
'
loading report view
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
({
props
:
{
...
defaultProps
,
canEdit
:
true
,
},
loading
:
false
,
});
wrapper
.
setData
({
iteration
,
});
});
it
(
'
updates URL when loading form
'
,
async
()
=>
{
jest
.
spyOn
(
window
.
history
,
'
pushState
'
).
mockImplementation
(()
=>
{});
clickEditButton
();
await
wrapper
.
vm
.
$nextTick
();
expect
(
window
.
history
.
pushState
).
toHaveBeenCalledWith
(
{
prev
:
'
viewIteration
'
},
null
,
'
/edit
'
,
);
});
});
describe
(
'
loading edit form directly
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
({
props
:
{
...
defaultProps
,
canEdit
:
true
,
initiallyEditing
:
true
,
},
loading
:
false
,
});
wrapper
.
setData
({
iteration
,
});
});
it
(
'
updates URL when cancelling form submit
'
,
async
()
=>
{
jest
.
spyOn
(
window
.
history
,
'
pushState
'
).
mockImplementation
(()
=>
{});
findIterationForm
().
vm
.
$emit
(
'
cancel
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
window
.
history
.
pushState
).
toHaveBeenCalledWith
(
{
prev
:
'
editIteration
'
},
null
,
'
/
'
,
);
});
it
(
'
updates URL after form submitted
'
,
async
()
=>
{
jest
.
spyOn
(
window
.
history
,
'
pushState
'
).
mockImplementation
(()
=>
{});
findIterationForm
().
vm
.
$emit
(
'
updated
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
window
.
history
.
pushState
).
toHaveBeenCalledWith
(
{
prev
:
'
editIteration
'
},
null
,
'
/
'
,
);
});
});
});
describe
(
'
actions dropdown to edit iteration
'
,
()
=>
{
describe
.
each
`
description | canEdit | namespaceType | canEditIteration
${
'
has permissions
'
}
|
${
true
}
|
${
Namespace
.
Group
}
|
${
true
}
${
'
has permissions
'
}
|
${
true
}
|
${
Namespace
.
Project
}
|
${
false
}
${
'
does not have permissions
'
}
|
${
false
}
|
${
Namespace
.
Group
}
|
${
false
}
${
'
does not have permissions
'
}
|
${
false
}
|
${
Namespace
.
Project
}
|
${
false
}
`
(
'
when user $description and they are viewing an iteration within a $namespaceType
'
,
({
canEdit
,
namespaceType
,
canEditIteration
})
=>
{
beforeEach
(()
=>
{
mountComponent
({
props
:
{
...
defaultProps
,
canEdit
,
namespaceType
,
},
});
wrapper
.
setData
({
iteration
,
});
});
it
(
`
${
canEditIteration
?
'
is shown
'
:
'
is hidden
'
}
`
,
()
=>
{
expect
(
wrapper
.
findComponent
(
GlDropdown
).
exists
()).
toBe
(
canEditIteration
);
});
},
);
});
});
});
ee/spec/frontend/iterations/mock_data.js
View file @
d136ebfe
export
const
mockIterationNode
=
{
description
:
'
some description
'
,
descriptionHtml
:
'
<p></p>
'
,
descriptionHtml
:
'
<p>
some description
</p>
'
,
dueDate
:
'
2021-02-17
'
,
id
:
'
gid://gitlab/Iteration/4
'
,
iid
:
'
1
'
,
...
...
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