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
c5d3bcff
Commit
c5d3bcff
authored
Apr 30, 2021
by
Natalia Tepluhina
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'psi-cadence-create-form' into 'master'
Add cadence form FE See merge request gitlab-org/gitlab!59227
parents
e38498d2
6cfe8f32
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
574 additions
and
4 deletions
+574
-4
ee/app/assets/javascripts/iterations/components/iteration_cadence_form.vue
...ascripts/iterations/components/iteration_cadence_form.vue
+320
-1
ee/app/assets/javascripts/iterations/index.js
ee/app/assets/javascripts/iterations/index.js
+5
-3
ee/app/assets/javascripts/iterations/queries/create_cadence.mutation.graphql
...cripts/iterations/queries/create_cadence.mutation.graphql
+9
-0
ee/spec/frontend/iterations/components/iteration_cadence_form_spec.js
...tend/iterations/components/iteration_cadence_form_spec.js
+192
-0
locale/gitlab.pot
locale/gitlab.pot
+48
-0
No files found.
ee/app/assets/javascripts/iterations/components/iteration_cadence_form.vue
View file @
c5d3bcff
<
script
>
import
{
GlAlert
,
GlButton
,
GlDatepicker
,
GlForm
,
GlFormCheckbox
,
GlFormGroup
,
GlFormInput
,
GlFormSelect
,
}
from
'
@gitlab/ui
'
;
import
{
visitUrl
}
from
'
~/lib/utils/url_utility
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
createCadence
from
'
../queries/create_cadence.mutation.graphql
'
;
const
i18n
=
Object
.
freeze
({
title
:
{
label
:
s__
(
'
Iterations|Title
'
),
placeholder
:
s__
(
'
Iterations|Cadence name
'
),
},
automatedScheduling
:
{
label
:
s__
(
'
Iterations|Automated scheduling
'
),
description
:
s__
(
'
Iterations|Iteration scheduling will be handled automatically
'
),
},
startDate
:
{
label
:
s__
(
'
Iterations|Start date
'
),
placeholder
:
s__
(
'
Iterations|Select start date
'
),
description
:
s__
(
'
Iterations|The start date of your first iteration
'
),
},
duration
:
{
label
:
s__
(
'
Iterations|Duration
'
),
description
:
s__
(
'
Iterations|The duration for each iteration (in weeks)
'
),
placeholder
:
s__
(
'
Iterations|Select duration
'
),
},
futureIterations
:
{
label
:
s__
(
'
Iterations|Future iterations
'
),
description
:
s__
(
'
Iterations|Number of future iterations you would like to have scheduled
'
),
placeholder
:
s__
(
'
Iterations|Select number
'
),
},
pageTitle
:
s__
(
'
Iterations|New iteration cadence
'
),
create
:
s__
(
'
Iterations|Create cadence
'
),
cancel
:
__
(
'
Cancel
'
),
requiredField
:
__
(
'
This field is required.
'
),
});
export
default
{
availableDurations
:
[{
value
:
null
,
text
:
i18n
.
duration
.
placeholder
},
1
,
2
,
3
,
4
,
5
,
6
],
availableFutureIterations
:
[
{
value
:
null
,
text
:
i18n
.
futureIterations
.
placeholder
},
2
,
4
,
6
,
8
,
10
,
12
,
],
components
:
{
GlAlert
,
GlButton
,
GlDatepicker
,
GlForm
,
GlFormCheckbox
,
GlFormGroup
,
GlFormInput
,
GlFormSelect
,
},
inject
:
[
'
groupPath
'
],
props
:
{
cadencesListPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
data
()
{
return
{
cadences
:
[],
loading
:
false
,
errorMessage
:
''
,
title
:
''
,
automatic
:
true
,
startDate
:
null
,
durationInWeeks
:
null
,
rollOverIssues
:
false
,
iterationsInAdvance
:
null
,
validationState
:
{
title
:
null
,
startDate
:
null
,
durationInWeeks
:
null
,
iterationsInAdvance
:
null
,
},
i18n
,
};
},
computed
:
{
valid
()
{
return
!
Object
.
values
(
this
.
validationState
).
includes
(
false
);
},
variables
()
{
const
vars
=
{
input
:
{
groupPath
:
this
.
groupPath
,
title
:
this
.
title
,
automatic
:
this
.
automatic
,
startDate
:
this
.
startDate
,
durationInWeeks
:
this
.
durationInWeeks
,
active
:
true
,
},
};
if
(
this
.
automatic
)
{
vars
.
input
=
{
...
vars
.
input
,
iterationsInAdvance
:
this
.
iterationsInAdvance
,
};
}
return
vars
;
},
},
methods
:
{
validate
(
field
)
{
this
.
validationState
[
field
]
=
Boolean
(
this
[
field
]);
},
validateAllFields
()
{
Object
.
keys
(
this
.
validationState
)
.
filter
((
field
)
=>
{
if
(
this
.
automatic
)
{
return
true
;
}
const
requiredFieldsForAutomatedScheduling
=
[
'
iterationsInAdvance
'
];
return
!
requiredFieldsForAutomatedScheduling
.
includes
(
field
);
})
.
forEach
((
field
)
=>
{
this
.
validate
(
field
);
});
},
clearValidation
()
{
this
.
validationState
.
startDate
=
null
;
this
.
validationState
.
durationInWeeks
=
null
;
this
.
validationState
.
iterationsInAdvance
=
null
;
},
save
()
{
this
.
validateAllFields
();
if
(
!
this
.
valid
)
{
return
null
;
}
this
.
loading
=
true
;
return
this
.
createCadence
();
},
cancel
()
{
if
(
this
.
cadencesListPath
)
{
visitUrl
(
this
.
cadencesListPath
);
}
else
{
this
.
$emit
(
'
cancel
'
);
}
},
createCadence
()
{
return
this
.
$apollo
.
mutate
({
mutation
:
createCadence
,
variables
:
this
.
variables
,
})
.
then
(({
data
,
errors
:
topLevelErrors
=
[]
})
=>
{
if
(
topLevelErrors
.
length
>
0
)
{
this
.
errorMessage
=
topLevelErrors
[
0
].
message
;
return
;
}
const
{
errors
}
=
data
.
iterationCadenceCreate
;
if
(
errors
.
length
>
0
)
{
[
this
.
errorMessage
]
=
errors
;
return
;
}
visitUrl
(
this
.
cadencesListPath
);
})
.
catch
((
e
)
=>
{
this
.
errorMessage
=
__
(
'
Unable to save cadence. Please try again
'
);
throw
e
;
})
.
finally
(()
=>
{
this
.
loading
=
false
;
});
},
},
};
</
script
>
<
template
>
<
template
>
<div></div>
<article>
<div
class=
"gl-display-flex"
>
<h3
ref=
"pageTitle"
class=
"page-title"
>
{{
i18n
.
pageTitle
}}
</h3>
</div>
<gl-form>
<gl-alert
v-if=
"errorMessage"
class=
"gl-mb-5"
variant=
"danger"
@
dismiss=
"errorMessage = ''"
>
{{
errorMessage
}}
</gl-alert>
<gl-form-group
:label=
"i18n.title.label"
:label-cols-md=
"2"
label-class=
"text-right-md gl-pt-3!"
label-for=
"cadence-title"
:invalid-feedback=
"i18n.requiredField"
:state=
"validationState.title"
>
<gl-form-input
id=
"cadence-title"
v-model=
"title"
autocomplete=
"off"
data-qa-selector=
"iteration_cadence_title_field"
:placeholder=
"i18n.title.placeholder"
size=
"xl"
:state=
"validationState.title"
@
blur=
"validate('title')"
/>
</gl-form-group>
<gl-form-group
:label-cols-md=
"2"
label-class=
"gl-font-weight-bold text-right-md gl-pt-3!"
label-for=
"cadence-automated-scheduling"
:description=
"i18n.automatedScheduling.description"
>
<gl-form-checkbox
id=
"cadence-automated-scheduling"
v-model=
"automatic"
@
change=
"clearValidation"
>
<span
class=
"gl-font-weight-bold"
>
{{
i18n
.
automatedScheduling
.
label
}}
</span>
</gl-form-checkbox>
</gl-form-group>
<gl-form-group
:label=
"i18n.startDate.label"
:label-cols-md=
"2"
label-class=
"text-right-md gl-pt-3!"
label-for=
"cadence-start-date"
:description=
"i18n.startDate.description"
:invalid-feedback=
"i18n.requiredField"
:state=
"validationState.startDate"
>
<gl-datepicker
:target=
"null"
>
<gl-form-input
id=
"cadence-start-date"
v-model=
"startDate"
:placeholder=
"i18n.startDate.placeholder"
class=
"datepicker gl-datepicker-input"
autocomplete=
"off"
inputmode=
"none"
required
:state=
"validationState.startDate"
data-qa-selector=
"cadence_start_date"
@
blur=
"validate('startDate')"
/>
</gl-datepicker>
</gl-form-group>
<gl-form-group
:label=
"i18n.duration.label"
:label-cols-md=
"2"
label-class=
"text-right-md gl-pt-3!"
label-for=
"cadence-duration"
:description=
"i18n.duration.description"
:invalid-feedback=
"i18n.requiredField"
:state=
"validationState.durationInWeeks"
>
<gl-form-select
id=
"cadence-duration"
v-model.number=
"durationInWeeks"
:options=
"$options.availableDurations"
class=
"gl-form-input-md"
required
data-qa-selector=
"iteration_cadence_name_field"
@
change=
"validate('durationInWeeks')"
/>
</gl-form-group>
<gl-form-group
:label=
"i18n.futureIterations.label"
:label-cols-md=
"2"
:content-cols-md=
"2"
label-class=
"text-right-md gl-pt-3!"
label-for=
"cadence-schedule-future-iterations"
:description=
"i18n.futureIterations.description"
:invalid-feedback=
"i18n.requiredField"
:state=
"validationState.iterationsInAdvance"
>
<gl-form-select
id=
"cadence-schedule-future-iterations"
v-model.number=
"iterationsInAdvance"
:disabled=
"!automatic"
:options=
"$options.availableFutureIterations"
:required=
"automatic"
class=
"gl-form-input-md"
data-qa-selector=
"iteration_cadence_name_field"
@
change=
"validate('iterationsInAdvance')"
/>
</gl-form-group>
<div
class=
"form-actions gl-display-flex"
>
<gl-button
:loading=
"loading"
data-testid=
"save-cadence"
variant=
"confirm"
data-qa-selector=
"save_cadence_button"
@
click=
"save"
>
{{
i18n
.
create
}}
</gl-button>
<gl-button
class=
"ml-auto"
data-testid=
"cancel-create-cadence"
@
click=
"cancel"
>
{{
i18n
.
cancel
}}
</gl-button>
</div>
</gl-form>
</article>
</
template
>
</
template
>
ee/app/assets/javascripts/iterations/index.js
View file @
c5d3bcff
...
@@ -98,17 +98,19 @@ export function initCadenceForm() {
...
@@ -98,17 +98,19 @@ export function initCadenceForm() {
return
null
;
return
null
;
}
}
const
{
groupFullPath
:
groupPath
,
cadenceId
,
cadence
ListPath
}
=
el
;
const
{
groupFullPath
:
groupPath
,
cadenceId
,
cadence
sListPath
}
=
el
.
dataset
;
return
new
Vue
({
return
new
Vue
({
el
,
el
,
apolloProvider
,
apolloProvider
,
provide
:
{
groupPath
,
},
render
(
createElement
)
{
render
(
createElement
)
{
return
createElement
(
IterationCadenceForm
,
{
return
createElement
(
IterationCadenceForm
,
{
props
:
{
props
:
{
groupPath
,
cadenceId
,
cadenceId
,
cadenceListPath
,
cadence
s
ListPath
,
},
},
});
});
},
},
...
...
ee/app/assets/javascripts/iterations/queries/create_cadence.mutation.graphql
0 → 100644
View file @
c5d3bcff
mutation
createIterationCadence
(
$input
:
IterationCadenceCreateInput
!)
{
iterationCadenceCreate
(
input
:
$input
)
{
iterationCadence
{
id
title
}
errors
}
}
ee/spec/frontend/iterations/components/iteration_cadence_form_spec.js
0 → 100644
View file @
c5d3bcff
import
{
GlFormCheckbox
,
GlFormGroup
}
from
'
@gitlab/ui
'
;
import
{
createLocalVue
,
mount
}
from
'
@vue/test-utils
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
IterationCadenceForm
from
'
ee/iterations/components/iteration_cadence_form.vue
'
;
import
createCadence
from
'
ee/iterations/queries/create_cadence.mutation.graphql
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
{
TEST_HOST
}
from
'
helpers/test_constants
'
;
import
{
extendedWrapper
}
from
'
helpers/vue_test_utils_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
visitUrl
}
from
'
~/lib/utils/url_utility
'
;
jest
.
mock
(
'
~/lib/utils/url_utility
'
);
const
localVue
=
createLocalVue
();
function
createMockApolloProvider
(
requestHandlers
)
{
localVue
.
use
(
VueApollo
);
return
createMockApollo
(
requestHandlers
);
}
describe
(
'
Iteration cadence form
'
,
()
=>
{
let
wrapper
;
const
groupPath
=
'
gitlab-org
'
;
const
id
=
72
;
const
iterationCadence
=
{
id
:
`gid://gitlab/Iteration/
${
id
}
`
,
title
:
'
An iteration
'
,
description
:
'
The words
'
,
startDate
:
'
2020-06-28
'
,
dueDate
:
'
2020-07-05
'
,
};
const
createMutationSuccess
=
{
data
:
{
iterationCadenceCreate
:
{
iterationCadence
,
errors
:
[]
}
},
};
const
createMutationFailure
=
{
data
:
{
iterationCadenceCreate
:
{
iterationCadence
,
errors
:
[
'
alas, your data is unchanged
'
]
},
},
};
const
defaultProps
=
{
cadencesListPath
:
TEST_HOST
};
function
createComponent
({
props
=
defaultProps
,
resolverMock
}
=
{})
{
const
apolloProvider
=
createMockApolloProvider
([[
createCadence
,
resolverMock
]]);
wrapper
=
extendedWrapper
(
mount
(
IterationCadenceForm
,
{
apolloProvider
,
localVue
,
propsData
:
props
,
provide
:
{
groupPath
,
},
}),
);
}
afterEach
(()
=>
{
wrapper
.
destroy
();
});
const
findTitleGroup
=
()
=>
wrapper
.
findAllComponents
(
GlFormGroup
).
at
(
0
);
const
findAutomatedSchedulingGroup
=
()
=>
wrapper
.
findAllComponents
(
GlFormGroup
).
at
(
1
);
const
findStartDateGroup
=
()
=>
wrapper
.
findAllComponents
(
GlFormGroup
).
at
(
2
);
const
findDurationGroup
=
()
=>
wrapper
.
findAllComponents
(
GlFormGroup
).
at
(
3
);
const
findFutureIterationsGroup
=
()
=>
wrapper
.
findAllComponents
(
GlFormGroup
).
at
(
4
);
const
findTitle
=
()
=>
wrapper
.
find
(
'
#cadence-title
'
);
const
findStartDate
=
()
=>
wrapper
.
find
(
'
#cadence-start-date
'
);
const
findFutureIterations
=
()
=>
wrapper
.
find
(
'
#cadence-schedule-future-iterations
'
);
const
findDuration
=
()
=>
wrapper
.
find
(
'
#cadence-duration
'
);
const
findSaveButton
=
()
=>
wrapper
.
findByTestId
(
'
save-cadence
'
);
const
findCancelButton
=
()
=>
wrapper
.
findByTestId
(
'
cancel-create-cadence
'
);
const
clickSave
=
()
=>
findSaveButton
().
vm
.
$emit
(
'
click
'
);
const
clickCancel
=
()
=>
findCancelButton
().
vm
.
$emit
(
'
click
'
);
describe
(
'
Create cadence
'
,
()
=>
{
let
resolverMock
;
beforeEach
(()
=>
{
resolverMock
=
jest
.
fn
().
mockResolvedValue
(
createMutationSuccess
);
createComponent
({
resolverMock
});
});
it
(
'
cancel button links to list page
'
,
()
=>
{
clickCancel
();
expect
(
visitUrl
).
toHaveBeenCalledWith
(
TEST_HOST
);
});
describe
(
'
save
'
,
()
=>
{
it
(
'
triggers mutation with form data
'
,
()
=>
{
const
title
=
'
Iteration 5
'
;
const
startDate
=
'
2020-05-05
'
;
const
durationInWeeks
=
2
;
const
iterationsInAdvance
=
6
;
findTitle
().
vm
.
$emit
(
'
input
'
,
title
);
findStartDate
().
vm
.
$emit
(
'
input
'
,
startDate
);
findDuration
().
vm
.
$emit
(
'
input
'
,
durationInWeeks
);
findFutureIterations
().
vm
.
$emit
(
'
input
'
,
iterationsInAdvance
);
clickSave
();
expect
(
resolverMock
).
toHaveBeenCalledWith
({
input
:
{
groupPath
,
title
,
automatic
:
true
,
startDate
,
durationInWeeks
,
iterationsInAdvance
,
active
:
true
,
},
});
});
it
(
'
redirects to Iteration page on success
'
,
async
()
=>
{
const
title
=
'
Iteration 5
'
;
const
startDate
=
'
2020-05-05
'
;
const
durationInWeeks
=
2
;
const
iterationsInAdvance
=
6
;
findTitle
().
vm
.
$emit
(
'
input
'
,
title
);
findStartDate
().
vm
.
$emit
(
'
input
'
,
startDate
);
findDuration
().
vm
.
$emit
(
'
input
'
,
durationInWeeks
);
findFutureIterations
().
vm
.
$emit
(
'
input
'
,
iterationsInAdvance
);
clickSave
();
await
waitForPromises
();
expect
(
visitUrl
).
toHaveBeenCalled
();
});
it
(
'
does not submit if required fields missing
'
,
()
=>
{
clickSave
();
expect
(
resolverMock
).
not
.
toHaveBeenCalled
();
expect
(
findTitleGroup
().
text
()).
toContain
(
'
This field is required
'
);
expect
(
findStartDateGroup
().
text
()).
toContain
(
'
This field is required
'
);
expect
(
findDurationGroup
().
text
()).
toContain
(
'
This field is required
'
);
expect
(
findFutureIterationsGroup
().
text
()).
toContain
(
'
This field is required
'
);
});
it
(
'
loading=false on error
'
,
async
()
=>
{
resolverMock
=
jest
.
fn
().
mockResolvedValue
(
createMutationFailure
);
createComponent
({
resolverMock
});
clickSave
();
await
waitForPromises
();
expect
(
findSaveButton
().
props
(
'
loading
'
)).
toBe
(
false
);
});
});
describe
(
'
automated scheduling disabled
'
,
()
=>
{
beforeEach
(()
=>
{
findAutomatedSchedulingGroup
().
find
(
GlFormCheckbox
).
vm
.
$emit
(
'
input
'
,
false
);
});
it
(
'
disables future iterations
'
,
()
=>
{
expect
(
findFutureIterations
().
attributes
(
'
disabled
'
)).
toBe
(
'
disabled
'
);
});
it
(
'
does not require future iterations
'
,
()
=>
{
const
title
=
'
Iteration 5
'
;
const
startDate
=
'
2020-05-05
'
;
const
durationInWeeks
=
2
;
findTitle
().
vm
.
$emit
(
'
input
'
,
title
);
findStartDate
().
vm
.
$emit
(
'
input
'
,
startDate
);
findDuration
().
vm
.
$emit
(
'
input
'
,
durationInWeeks
);
clickSave
();
expect
(
resolverMock
).
toHaveBeenCalledWith
({
input
:
{
groupPath
,
title
,
automatic
:
false
,
startDate
,
durationInWeeks
,
active
:
true
,
},
});
});
});
});
});
locale/gitlab.pot
View file @
c5d3bcff
...
@@ -18231,6 +18231,51 @@ msgstr ""
...
@@ -18231,6 +18231,51 @@ msgstr ""
msgid "Iterations"
msgid "Iterations"
msgstr ""
msgstr ""
msgid "Iterations|Automated scheduling"
msgstr ""
msgid "Iterations|Cadence name"
msgstr ""
msgid "Iterations|Create cadence"
msgstr ""
msgid "Iterations|Duration"
msgstr ""
msgid "Iterations|Future iterations"
msgstr ""
msgid "Iterations|Iteration scheduling will be handled automatically"
msgstr ""
msgid "Iterations|New iteration cadence"
msgstr ""
msgid "Iterations|Number of future iterations you would like to have scheduled"
msgstr ""
msgid "Iterations|Select duration"
msgstr ""
msgid "Iterations|Select number"
msgstr ""
msgid "Iterations|Select start date"
msgstr ""
msgid "Iterations|Start date"
msgstr ""
msgid "Iterations|The duration for each iteration (in weeks)"
msgstr ""
msgid "Iterations|The start date of your first iteration"
msgstr ""
msgid "Iterations|Title"
msgstr ""
msgid "Iteration|Dates cannot overlap with other existing Iterations within this group"
msgid "Iteration|Dates cannot overlap with other existing Iterations within this group"
msgstr ""
msgstr ""
...
@@ -34073,6 +34118,9 @@ msgstr ""
...
@@ -34073,6 +34118,9 @@ msgstr ""
msgid "Unable to load the merge request widget. Try reloading the page."
msgid "Unable to load the merge request widget. Try reloading the page."
msgstr ""
msgstr ""
msgid "Unable to save cadence. Please try again"
msgstr ""
msgid "Unable to save iteration. Please try again"
msgid "Unable to save iteration. Please try again"
msgstr ""
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