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
54420c4f
Commit
54420c4f
authored
Jan 05, 2021
by
GitLab Bot
Browse files
Options
Browse Files
Download
Plain Diff
Automatic merge of gitlab-org/gitlab master
parents
1f25b49d
5f003f9d
Changes
22
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
1071 additions
and
873 deletions
+1071
-873
app/assets/javascripts/lib/utils/datetime_utility.js
app/assets/javascripts/lib/utils/datetime_utility.js
+17
-0
ee/app/assets/javascripts/oncall_schedules/components/oncall_schedule.vue
...vascripts/oncall_schedules/components/oncall_schedule.vue
+11
-8
ee/app/assets/javascripts/oncall_schedules/components/rotations/components/add_edit_rotation_form.vue
...omponents/rotations/components/add_edit_rotation_form.vue
+206
-0
ee/app/assets/javascripts/oncall_schedules/components/rotations/components/add_edit_rotation_modal.vue
...mponents/rotations/components/add_edit_rotation_modal.vue
+262
-0
ee/app/assets/javascripts/oncall_schedules/components/rotations/components/add_rotation_modal.vue
...es/components/rotations/components/add_rotation_modal.vue
+0
-304
ee/app/assets/javascripts/oncall_schedules/components/schedule/components/rotations_list_section.vue
...components/schedule/components/rotations_list_section.vue
+6
-1
ee/app/assets/javascripts/oncall_schedules/constants.js
ee/app/assets/javascripts/oncall_schedules/constants.js
+7
-3
ee/app/assets/javascripts/oncall_schedules/graphql/create_oncall_schedule_rotation.mutation.graphql
.../graphql/create_oncall_schedule_rotation.mutation.graphql
+0
-26
ee/app/assets/javascripts/oncall_schedules/graphql/fragments/oncall_schedule_rotation.fragment.graphql
...aphql/fragments/oncall_schedule_rotation.fragment.graphql
+17
-0
ee/app/assets/javascripts/oncall_schedules/graphql/mutations/create_oncall_schedule_rotation.mutation.graphql
...utations/create_oncall_schedule_rotation.mutation.graphql
+10
-0
ee/app/assets/javascripts/oncall_schedules/graphql/mutations/update_oncall_schedule_rotation.mutation.graphql
...utations/update_oncall_schedule_rotation.mutation.graphql
+10
-0
ee/app/assets/javascripts/oncall_schedules/utils/cache_updates.js
...ssets/javascripts/oncall_schedules/utils/cache_updates.js
+78
-1
ee/app/assets/javascripts/oncall_schedules/utils/error_messages.js
...sets/javascripts/oncall_schedules/utils/error_messages.js
+4
-0
ee/spec/frontend/oncall_schedule/mocks/apollo_mock.js
ee/spec/frontend/oncall_schedule/mocks/apollo_mock.js
+56
-0
ee/spec/frontend/oncall_schedule/rotations/components/__snapshots__/add_edit_rotation_modal_spec.js.snap
...onents/__snapshots__/add_edit_rotation_modal_spec.js.snap
+21
-0
ee/spec/frontend/oncall_schedule/rotations/components/__snapshots__/add_rotation_modal_spec.js.snap
.../components/__snapshots__/add_rotation_modal_spec.js.snap
+0
-522
ee/spec/frontend/oncall_schedule/rotations/components/add_edit_rotation_form_spec.js
...edule/rotations/components/add_edit_rotation_form_spec.js
+123
-0
ee/spec/frontend/oncall_schedule/rotations/components/add_edit_rotation_modal_spec.js
...dule/rotations/components/add_edit_rotation_modal_spec.js
+199
-0
ee/spec/frontend/oncall_schedule/schedule/components/__snapshots__/rotations_list_section_spec.js.snap
...ponents/__snapshots__/rotations_list_section_spec.js.snap
+4
-0
ee/spec/frontend/oncall_schedule/schedule/components/schedule_timeline_section_spec.js
...ule/schedule/components/schedule_timeline_section_spec.js
+6
-8
locale/gitlab.pot
locale/gitlab.pot
+9
-0
spec/frontend/lib/utils/datetime_utility_spec.js
spec/frontend/lib/utils/datetime_utility_spec.js
+25
-0
No files found.
app/assets/javascripts/lib/utils/datetime_utility.js
View file @
54420c4f
...
@@ -806,3 +806,20 @@ export const dateAtFirstDayOfMonth = (date) => new Date(newDate(date).setDate(1)
...
@@ -806,3 +806,20 @@ export const dateAtFirstDayOfMonth = (date) => new Date(newDate(date).setDate(1)
* @return {Boolean} true if the dates match
* @return {Boolean} true if the dates match
*/
*/
export
const
datesMatch
=
(
date1
,
date2
)
=>
differenceInMilliseconds
(
date1
,
date2
)
===
0
;
export
const
datesMatch
=
(
date1
,
date2
)
=>
differenceInMilliseconds
(
date1
,
date2
)
===
0
;
/**
* A utility function which computes a formatted 24 hour
* time string from a positive int in the range 0 - 24.
*
* @param {Int} time a positive Int between 0 and 24
*
* @returns {String} formatted 24 hour time String
*/
export
const
format24HourTimeStringFromInt
=
(
time
)
=>
{
if
(
!
Number
.
isInteger
(
time
)
||
time
<
0
||
time
>
24
)
{
return
''
;
}
const
formatted24HourString
=
time
>
9
?
`
${
time
}
:00`
:
`0
${
time
}
:00`
;
return
formatted24HourString
;
};
ee/app/assets/javascripts/oncall_schedules/components/oncall_schedule.vue
View file @
54420c4f
...
@@ -12,11 +12,10 @@ import { s__, __ } from '~/locale';
...
@@ -12,11 +12,10 @@ import { s__, __ } from '~/locale';
import
ScheduleTimelineSection
from
'
./schedule/components/schedule_timeline_section.vue
'
;
import
ScheduleTimelineSection
from
'
./schedule/components/schedule_timeline_section.vue
'
;
import
DeleteScheduleModal
from
'
./delete_schedule_modal.vue
'
;
import
DeleteScheduleModal
from
'
./delete_schedule_modal.vue
'
;
import
EditScheduleModal
from
'
./add_edit_schedule_modal.vue
'
;
import
EditScheduleModal
from
'
./add_edit_schedule_modal.vue
'
;
import
AddRotationModal
from
'
./rotations/components/add_rotation_modal.vue
'
;
import
AddEditRotationModal
from
'
./rotations/components/add_edit_rotation_modal.vue
'
;
import
{
getTimeframeForWeeksView
}
from
'
./schedule/utils
'
;
import
{
PRESET_TYPES
}
from
'
../constants
'
;
import
RotationsListSection
from
'
./schedule/components/rotations_list_section.vue
'
;
import
RotationsListSection
from
'
./schedule/components/rotations_list_section.vue
'
;
import
{
getTimeframeForWeeksView
}
from
'
./schedule/utils
'
;
import
{
addRotationModalId
,
editRotationModalId
,
PRESET_TYPES
}
from
'
../constants
'
;
export
const
i18n
=
{
export
const
i18n
=
{
scheduleForTz
:
s__
(
'
OnCallSchedules|On-call schedule for the %{timezone}
'
),
scheduleForTz
:
s__
(
'
OnCallSchedules|On-call schedule for the %{timezone}
'
),
...
@@ -25,14 +24,13 @@ export const i18n = {
...
@@ -25,14 +24,13 @@ export const i18n = {
rotationTitle
:
s__
(
'
OnCallSchedules|Rotations
'
),
rotationTitle
:
s__
(
'
OnCallSchedules|Rotations
'
),
addARotation
:
s__
(
'
OnCallSchedules|Add a rotation
'
),
addARotation
:
s__
(
'
OnCallSchedules|Add a rotation
'
),
};
};
export
const
addRotationModalId
=
'
addRotationModal
'
;
export
const
editScheduleModalId
=
'
editScheduleModal
'
;
export
const
editScheduleModalId
=
'
editScheduleModal
'
;
export
const
deleteScheduleModalId
=
'
deleteScheduleModal
'
;
export
const
deleteScheduleModalId
=
'
deleteScheduleModal
'
;
export
default
{
export
default
{
i18n
,
i18n
,
addRotationModalId
,
addRotationModalId
,
editRotationModalId
,
editScheduleModalId
,
editScheduleModalId
,
deleteScheduleModalId
,
deleteScheduleModalId
,
presetType
:
PRESET_TYPES
.
WEEKS
,
presetType
:
PRESET_TYPES
.
WEEKS
,
...
@@ -45,7 +43,7 @@ export default {
...
@@ -45,7 +43,7 @@ export default {
GlButton
,
GlButton
,
DeleteScheduleModal
,
DeleteScheduleModal
,
EditScheduleModal
,
EditScheduleModal
,
AddRotationModal
,
Add
Edit
RotationModal
,
RotationsListSection
,
RotationsListSection
,
},
},
directives
:
{
directives
:
{
...
@@ -151,6 +149,11 @@ export default {
...
@@ -151,6 +149,11 @@ export default {
:modal-id=
"$options.editScheduleModalId"
:modal-id=
"$options.editScheduleModalId"
is-edit-mode
is-edit-mode
/>
/>
<add-rotation-modal
:schedule=
"schedule"
:modal-id=
"$options.addRotationModalId"
/>
<add-edit-rotation-modal
:schedule=
"schedule"
:modal-id=
"$options.addRotationModalId"
/>
<add-edit-rotation-modal
:schedule=
"schedule"
:modal-id=
"$options.editRotationModalId"
is-edit-mode
/>
</div>
</div>
</template>
</template>
ee/app/assets/javascripts/oncall_schedules/components/rotations/components/add_edit_rotation_form.vue
0 → 100644
View file @
54420c4f
<
script
>
import
{
GlForm
,
GlFormGroup
,
GlFormInput
,
GlDropdown
,
GlDropdownItem
,
GlDatepicker
,
GlTokenSelector
,
GlAvatar
,
GlAvatarLabeled
,
}
from
'
@gitlab/ui
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
{
LENGTH_ENUM
,
HOURS_IN_DAY
,
CHEVRON_SKIPPING_SHADE_ENUM
,
CHEVRON_SKIPPING_PALETTE_ENUM
,
}
from
'
../../../constants
'
;
import
{
format24HourTimeStringFromInt
}
from
'
~/lib/utils/datetime_utility
'
;
export
const
i18n
=
{
selectParticipant
:
s__
(
'
OnCallSchedules|Select participant
'
),
errorMsg
:
s__
(
'
OnCallSchedules|Failed to add rotation
'
),
fields
:
{
name
:
{
title
:
__
(
'
Name
'
),
error
:
s__
(
'
OnCallSchedules|Rotation name cannot be empty
'
)
},
participants
:
{
title
:
__
(
'
Participants
'
),
error
:
s__
(
'
OnCallSchedules|Rotation participants cannot be empty
'
),
},
rotationLength
:
{
title
:
s__
(
'
OnCallSchedules|Rotation length
'
)
},
startsAt
:
{
title
:
__
(
'
Starts on
'
),
error
:
s__
(
'
OnCallSchedules|Rotation start date cannot be empty
'
),
},
},
};
export
default
{
i18n
,
HOURS_IN_DAY
,
tokenColorPalette
:
{
shade
:
CHEVRON_SKIPPING_SHADE_ENUM
,
palette
:
CHEVRON_SKIPPING_PALETTE_ENUM
,
},
LENGTH_ENUM
,
inject
:
[
'
projectPath
'
],
components
:
{
GlForm
,
GlFormGroup
,
GlFormInput
,
GlDropdown
,
GlDropdownItem
,
GlDatepicker
,
GlTokenSelector
,
GlAvatar
,
GlAvatarLabeled
,
},
props
:
{
form
:
{
type
:
Object
,
required
:
true
,
},
isLoading
:
{
type
:
Boolean
,
required
:
true
,
},
rotationNameIsValid
:
{
type
:
Boolean
,
required
:
true
,
},
rotationParticipantsAreValid
:
{
type
:
Boolean
,
required
:
true
,
},
rotationStartsAtIsValid
:
{
type
:
Boolean
,
required
:
true
,
},
participants
:
{
type
:
Array
,
required
:
true
,
},
schedule
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
},
data
()
{
return
{
participantsArr
:
[],
};
},
methods
:
{
format24HourTimeStringFromInt
,
},
};
</
script
>
<
template
>
<gl-form
class=
"w-75 gl-xs-w-full!"
@
submit.prevent=
"createRotation"
>
<gl-form-group
:label=
"$options.i18n.fields.name.title"
label-size=
"sm"
label-for=
"rotation-name"
:invalid-feedback=
"$options.i18n.fields.name.error"
:state=
"rotationNameIsValid"
>
<gl-form-input
id=
"rotation-name"
@
input=
"$emit('update-rotation-form',
{ type: 'name', value: $event })"
/>
</gl-form-group>
<gl-form-group
:label=
"$options.i18n.fields.participants.title"
label-size=
"sm"
label-for=
"rotation-participants"
:invalid-feedback=
"$options.i18n.fields.participants.error"
:state=
"rotationParticipantsAreValid"
>
<gl-token-selector
v-model=
"participantsArr"
:dropdown-items=
"participants"
:loading=
"isLoading"
container-class=
"gl-h-13! gl-overflow-y-auto"
@
text-input=
"$emit('filter-participants', $event)"
@
input=
"$emit('update-rotation-form',
{ type: 'participants', value: participantsArr })"
>
<template
#token-content
="
{ token }">
<gl-avatar
v-if=
"token.avatarUrl"
:src=
"token.avatarUrl"
:size=
"16"
/>
{{
token
.
name
}}
</
template
>
<
template
#dropdown-item-content=
"{ dropdownItem }"
>
<gl-avatar-labeled
:src=
"dropdownItem.avatarUrl"
:size=
"32"
:label=
"dropdownItem.name"
:sub-label=
"dropdownItem.username"
/>
</
template
>
</gl-token-selector>
</gl-form-group>
<gl-form-group
:label=
"$options.i18n.fields.rotationLength.title"
label-size=
"sm"
label-for=
"rotation-length"
>
<div
class=
"gl-display-flex"
>
<gl-form-input
id=
"rotation-length"
type=
"number"
class=
"gl-w-12 gl-mr-3"
min=
"1"
:value=
"1"
@
input=
"$emit('update-rotation-form', { type: 'rotationLength.length', value: $event })"
/>
<gl-dropdown
id=
"rotation-length"
:text=
"form.rotationLength.unit.toLowerCase()"
>
<gl-dropdown-item
v-for=
"unit in $options.LENGTH_ENUM"
:key=
"unit"
:is-checked=
"form.rotationLength.unit === unit"
is-check-item
@
click=
"$emit('update-rotation-form', { type: 'rotationLength.unit', value: unit })"
>
{{ unit.toLowerCase() }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</gl-form-group>
<gl-form-group
:label=
"$options.i18n.fields.startsAt.title"
label-size=
"sm"
label-for=
"rotation-time"
:invalid-feedback=
"$options.i18n.fields.startsAt.error"
:state=
"rotationStartsAtIsValid"
>
<div
class=
"gl-display-flex gl-align-items-center"
>
<gl-datepicker
class=
"gl-mr-3"
@
input=
"$emit('update-rotation-form', { type: 'startsAt.date', value: $event })"
/>
<span>
{{ __('at') }}
</span>
<gl-dropdown
id=
"rotation-time"
:text=
"format24HourTimeStringFromInt(form.startsAt.time)"
class=
"gl-w-12 gl-pl-3"
>
<gl-dropdown-item
v-for=
"time in $options.HOURS_IN_DAY"
:key=
"time"
:is-checked=
"form.startsAt.time === time"
is-check-item
@
click=
"$emit('update-rotation-form', { type: 'startsAt.time', value: time })"
>
<span
class=
"gl-white-space-nowrap"
>
{{ format24HourTimeStringFromInt(time) }}
</span>
</gl-dropdown-item>
</gl-dropdown>
<span
class=
"gl-pl-5"
>
{{ schedule.timezone }}
</span>
</div>
</gl-form-group>
</gl-form>
</template>
ee/app/assets/javascripts/oncall_schedules/components/rotations/components/add_edit_rotation_modal.vue
0 → 100644
View file @
54420c4f
<
script
>
import
{
GlModal
,
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
set
}
from
'
lodash
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
createFlash
,
{
FLASH_TYPES
}
from
'
~/flash
'
;
import
usersSearchQuery
from
'
~/graphql_shared/queries/users_search.query.graphql
'
;
import
getOncallSchedulesQuery
from
'
../../../graphql/queries/get_oncall_schedules.query.graphql
'
;
import
createOncallScheduleRotationMutation
from
'
../../../graphql/mutations/create_oncall_schedule_rotation.mutation.graphql
'
;
import
updateOncallScheduleRotationMutation
from
'
../../../graphql/mutations/update_oncall_schedule_rotation.mutation.graphql
'
;
import
{
LENGTH_ENUM
}
from
'
../../../constants
'
;
import
AddEditRotationForm
from
'
./add_edit_rotation_form.vue
'
;
import
{
updateStoreAfterRotationAdd
,
updateStoreAfterRotationEdit
,
}
from
'
../../../utils/cache_updates
'
;
import
{
format24HourTimeStringFromInt
}
from
'
~/lib/utils/datetime_utility
'
;
export
const
i18n
=
{
rotationCreated
:
s__
(
'
OnCallSchedules|Successfully created a new rotation
'
),
editedRotation
:
s__
(
'
OnCallSchedules|Successfully edited your rotation
'
),
addRotation
:
s__
(
'
OnCallSchedules|Add rotation
'
),
editRotation
:
s__
(
'
OnCallSchedules|Edit rotation
'
),
cancel
:
__
(
'
Cancel
'
),
};
export
default
{
i18n
,
LENGTH_ENUM
,
inject
:
[
'
projectPath
'
],
components
:
{
GlModal
,
GlAlert
,
AddEditRotationForm
,
},
props
:
{
modalId
:
{
type
:
String
,
required
:
true
,
},
isEditMode
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
schedule
:
{
type
:
Object
,
required
:
true
,
},
},
apollo
:
{
participants
:
{
query
:
usersSearchQuery
,
variables
()
{
return
{
search
:
this
.
ptSearchTerm
,
};
},
update
({
users
:
{
nodes
=
[]
}
=
{}
})
{
return
nodes
;
},
error
(
error
)
{
this
.
error
=
error
;
},
},
},
data
()
{
return
{
participants
:
[],
loading
:
false
,
ptSearchTerm
:
''
,
form
:
{
name
:
''
,
participants
:
[],
rotationLength
:
{
length
:
1
,
unit
:
this
.
$options
.
LENGTH_ENUM
.
hours
,
},
startsAt
:
{
date
:
null
,
time
:
0
,
},
},
error
:
''
,
};
},
computed
:
{
actionsProps
()
{
return
{
primary
:
{
text
:
this
.
title
,
attributes
:
[
{
variant
:
'
info
'
},
{
loading
:
this
.
loading
},
{
disabled
:
!
this
.
isFormValid
},
],
},
cancel
:
{
text
:
this
.
$options
.
i18n
.
cancel
,
},
};
},
rotationNameIsValid
()
{
return
this
.
form
.
name
!==
''
;
},
rotationParticipantsAreValid
()
{
return
this
.
form
.
participants
.
length
>
0
;
},
rotationStartsAtIsValid
()
{
return
Boolean
(
this
.
form
.
startsAt
.
date
);
},
rotationVariables
()
{
return
{
projectPath
:
this
.
projectPath
,
scheduleIid
:
this
.
schedule
.
iid
,
name
:
this
.
form
.
name
,
startsAt
:
{
...
this
.
form
.
startsAt
,
time
:
format24HourTimeStringFromInt
(
this
.
form
.
startsAt
.
time
),
},
rotationLength
:
{
...
this
.
form
.
rotationLength
,
length
:
parseInt
(
this
.
form
.
rotationLength
.
length
,
10
),
},
participants
:
this
.
form
.
participants
.
map
(({
username
})
=>
({
username
,
// eslint-disable-next-line @gitlab/require-i18n-strings
colorWeight
:
'
WEIGHT_500
'
,
colorPalette
:
'
BLUE
'
,
})),
};
},
isFormValid
()
{
return
(
this
.
rotationNameIsValid
&&
this
.
rotationParticipantsAreValid
&&
this
.
rotationStartsAtIsValid
);
},
isLoading
()
{
return
this
.
loading
||
this
.
$apollo
.
queries
.
participants
.
loading
;
},
title
()
{
return
this
.
isEditMode
?
this
.
$options
.
i18n
.
editRotation
:
this
.
$options
.
i18n
.
addRotation
;
},
},
methods
:
{
createRotation
()
{
this
.
loading
=
true
;
const
{
projectPath
,
schedule
}
=
this
;
this
.
$apollo
.
mutate
({
mutation
:
createOncallScheduleRotationMutation
,
variables
:
{
OncallRotationCreateInput
:
this
.
rotationVariables
},
update
(
store
,
{
data
})
{
updateStoreAfterRotationAdd
(
store
,
getOncallSchedulesQuery
,
data
,
schedule
.
iid
,
{
projectPath
,
});
},
})
.
then
(
({
data
:
{
oncallRotationCreate
:
{
errors
:
[
error
],
},
},
})
=>
{
if
(
error
)
{
throw
error
;
}
this
.
$refs
.
addEditScheduleRotationModal
.
hide
();
return
createFlash
({
message
:
this
.
$options
.
i18n
.
rotationCreated
,
type
:
FLASH_TYPES
.
SUCCESS
,
});
},
)
.
catch
((
error
)
=>
{
this
.
error
=
error
;
})
.
finally
(()
=>
{
this
.
loading
=
false
;
});
},
editRotation
()
{
this
.
loading
=
true
;
const
{
projectPath
,
schedule
}
=
this
;
this
.
$apollo
.
mutate
({
mutation
:
updateOncallScheduleRotationMutation
,
variables
:
{
OncallRotationUpdateInput
:
this
.
rotationVariables
},
update
(
store
,
{
data
})
{
updateStoreAfterRotationEdit
(
store
,
getOncallSchedulesQuery
,
data
,
schedule
.
iid
,
{
projectPath
,
});
},
})
.
then
(
({
data
:
{
oncallRotationUpdate
:
{
errors
:
[
error
],
},
},
})
=>
{
if
(
error
)
{
throw
error
;
}
this
.
$refs
.
addEditScheduleRotationModal
.
hide
();
return
createFlash
({
message
:
this
.
$options
.
i18n
.
editedRotation
,
type
:
FLASH_TYPES
.
SUCCESS
,
});
},
)
.
catch
((
error
)
=>
{
this
.
error
=
error
;
})
.
finally
(()
=>
{
this
.
loading
=
false
;
});
},
updateRotationForm
({
type
,
value
})
{
set
(
this
.
form
,
type
,
value
);
},
filterParticipants
(
query
)
{
this
.
ptSearchTerm
=
query
;
},
},
};
</
script
>
<
template
>
<gl-modal
ref=
"addEditScheduleRotationModal"
:modal-id=
"modalId"
size=
"sm"
:title=
"title"
:action-primary=
"actionsProps.primary"
:action-cancel=
"actionsProps.cancel"
@
primary.prevent=
"isEditMode ? editRotation() : createRotation()"
>
<gl-alert
v-if=
"error"
variant=
"danger"
@
dismiss=
"error = ''"
>
{{
error
||
$options
.
i18n
.
errorMsg
}}
</gl-alert>
<add-edit-rotation-form
:rotation-name-is-valid=
"rotationNameIsValid"
:rotation-participants-are-valid=
"rotationParticipantsAreValid"
:rotation-starts-at-is-valid=
"rotationStartsAtIsValid"
:form=
"form"
:schedule=
"schedule"
:participants=
"participants"
:is-loading=
"isLoading"
@
update-rotation-form=
"updateRotationForm"
@
filter-participants=
"filterParticipants"
/>
</gl-modal>
</
template
>
ee/app/assets/javascripts/oncall_schedules/components/rotations/components/add_rotation_modal.vue
deleted
100644 → 0
View file @
1f25b49d
<
script
>
import
{
GlModal
,
GlForm
,
GlFormGroup
,
GlFormInput
,
GlDropdown
,
GlDropdownItem
,
GlDatepicker
,
GlTokenSelector
,
GlAvatar
,
GlAvatarLabeled
,
GlAlert
,
}
from
'
@gitlab/ui
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
usersSearchQuery
from
'
~/graphql_shared/queries/users_search.query.graphql
'
;
import
createOncallScheduleRotationMutation
from
'
../../../graphql/create_oncall_schedule_rotation.mutation.graphql
'
;
import
{
LENGTH_ENUM
,
CHEVRON_SKIPPING_SHADE_ENUM
,
CHEVRON_SKIPPING_PALETTE_ENUM
,
}
from
'
../../../constants
'
;
export
default
{
i18n
:
{
selectParticipant
:
s__
(
'
OnCallSchedules|Select participant
'
),
addRotation
:
s__
(
'
OnCallSchedules|Add rotation
'
),
noResults
:
__
(
'
No matching results
'
),
cancel
:
__
(
'
Cancel
'
),
errorMsg
:
s__
(
'
OnCallSchedules|Failed to add rotation
'
),
fields
:
{
name
:
{
title
:
__
(
'
Name
'
),
error
:
s__
(
'
OnCallSchedules|Rotation name cannot be empty
'
)
},
participants
:
{
title
:
__
(
'
Participants
'
),
error
:
s__
(
'
OnCallSchedules|Rotation participants cannot be empty
'
),
},
length
:
{
title
:
s__
(
'
OnCallSchedules|Rotation length
'
)
},
startsOn
:
{
title
:
__
(
'
Starts on
'
),
error
:
s__
(
'
OnCallSchedules|Rotation start date cannot be empty
'
),
},
},
},
tokenColorPalette
:
{
shade
:
CHEVRON_SKIPPING_SHADE_ENUM
,
palette
:
CHEVRON_SKIPPING_PALETTE_ENUM
,
},
LENGTH_ENUM
,
inject
:
[
'
projectPath
'
],
components
:
{
GlModal
,
GlForm
,
GlFormGroup
,
GlFormInput
,
GlDropdown
,
GlDropdownItem
,
GlDatepicker
,
GlTokenSelector
,
GlAvatar
,
GlAvatarLabeled
,
GlAlert
,
},
props
:
{
modalId
:
{
type
:
String
,
required
:
true
,
},
schedule
:
{
type
:
Object
,
required
:
true
,
},
},
apollo
:
{
participants
:
{
query
:
usersSearchQuery
,
variables
()
{
return
{
search
:
this
.
ptSearchTerm
,
};
},
update
({
users
:
{
nodes
=
[]
}
=
{}
})
{
return
nodes
;
},
error
(
error
)
{
this
.
error
=
error
;
},
},
},
data
()
{
return
{
participants
:
[],
loading
:
false
,
ptSearchTerm
:
''
,
form
:
{
name
:
''
,
participants
:
[],
length
:
{
value
:
1
,
type
:
this
.
$options
.
LENGTH_ENUM
.
hours
,
},
startsOn
:
{
date
:
null
,
time
:
0
,
},
},
error
:
null
,
validationState
:
{
name
:
true
,
participants
:
true
,
startsOn
:
true
,
},
};
},
computed
:
{
actionsProps
()
{
return
{
primary
:
{
text
:
this
.
$options
.
i18n
.
addRotation
,
attributes
:
[{
variant
:
'
info
'
},
{
loading
:
this
.
loading
}],
},
cancel
:
{
text
:
this
.
$options
.
i18n
.
cancel
,
},
};
},
noResults
()
{
return
this
.
participants
.
length
===
0
;
},
},
methods
:
{
createRotation
()
{
this
.
loading
=
true
;
this
.
$apollo
.
mutate
({
mutation
:
createOncallScheduleRotationMutation
,
variables
:
{
oncallScheduleRotationCreate
:
{
projectPath
:
this
.
projectPath
,
...
this
.
form
,
},
},
})
.
then
(
({
data
:
{
oncallScheduleRotationCreate
:
{
errors
:
[
error
],
},
},
})
=>
{
if
(
error
)
{
throw
error
;
}
this
.
$refs
.
createScheduleModal
.
hide
();
},
)
.
catch
((
error
)
=>
{
this
.
error
=
error
;
})
.
finally
(()
=>
{
this
.
loading
=
false
;
});
},
formatTime
(
time
)
{
return
time
>
9
?
`
${
time
}
:00`
:
`0
${
time
}
:00`
;
},
filterParticipants
(
query
)
{
this
.
ptSearchTerm
=
query
;
},
setRotationLengthType
(
type
)
{
this
.
form
.
length
.
type
=
type
;
},
setRotationStartsOnTime
(
time
)
{
this
.
form
.
startsOn
.
time
=
time
;
},
validateForm
(
key
)
{
if
(
key
===
'
name
'
)
{
this
.
validationState
.
name
=
this
.
form
.
name
!==
''
;
}
else
if
(
key
===
'
participants
'
)
{
this
.
validationState
.
participants
=
this
.
form
.
participants
.
length
>
0
;
}
else
if
(
key
===
'
startsOn
'
)
{
this
.
validationState
.
startsOn
=
this
.
form
.
startsOn
.
date
!==
null
;
}
},
},
};
</
script
>
<
template
>
<gl-modal
ref=
"createScheduleRotationModal"
:modal-id=
"modalId"
size=
"sm"
:title=
"$options.i18n.addRotation"
:action-primary=
"actionsProps.primary"
:action-cancel=
"actionsProps.cancel"
@
primary=
"createRotation"
>
<gl-alert
v-if=
"error"
variant=
"danger"
@
dismiss=
"error = null"
>
{{
error
||
$options
.
i18n
.
errorMsg
}}
</gl-alert>
<gl-form
class=
"w-75 gl-xs-w-full!"
@
submit.prevent=
"createRotation"
>
<gl-form-group
:label=
"$options.i18n.fields.name.title"
label-size=
"sm"
label-for=
"rotation-name"
:invalid-feedback=
"$options.i18n.fields.name.error"
:state=
"validationState.name"
>
<gl-form-input
id=
"rotation-name"
v-model=
"form.name"
@
blur.native=
"validateForm('name')"
/>
</gl-form-group>
<gl-form-group
:label=
"$options.i18n.fields.participants.title"
label-size=
"sm"
label-for=
"rotation-participants"
:invalid-feedback=
"$options.i18n.fields.participants.error"
:state=
"validationState.participants"
>
<gl-token-selector
v-model=
"form.participants"
:dropdown-items=
"participants"
:loading=
"this.$apollo.queries.participants.loading"
:container-class=
"'gl-h-13! gl-overflow-y-auto'"
@
text-input=
"filterParticipants"
@
blur=
"validateForm('participants')"
>
<template
#token-content
="
{ token }">
<gl-avatar
v-if=
"token.avatarUrl"
:src=
"token.avatarUrl"
:size=
"16"
/>
{{
token
.
name
}}
</
template
>
<
template
#dropdown-item-content=
"{ dropdownItem }"
>
<gl-avatar-labeled
:src=
"dropdownItem.avatarUrl"
:size=
"32"
:label=
"dropdownItem.name"
:sub-label=
"dropdownItem.username"
/>
</
template
>
</gl-token-selector>
</gl-form-group>
<gl-form-group
:label=
"$options.i18n.fields.length.title"
label-size=
"sm"
label-for=
"rotation-length"
>
<div
class=
"gl-display-flex"
>
<gl-form-input
id=
"rotation-length"
v-model=
"form.length.value"
type=
"number"
class=
"gl-w-12 gl-mr-3"
min=
"1"
/>
<gl-dropdown
id=
"rotation-length"
:text=
"form.length.type"
>
<gl-dropdown-item
v-for=
"type in $options.LENGTH_ENUM"
:key=
"type"
:is-checked=
"form.length.type === type"
is-check-item
@
click=
"setRotationLengthType(type)"
>
{{ type }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</gl-form-group>
<gl-form-group
:label=
"$options.i18n.fields.startsOn.title"
label-size=
"sm"
label-for=
"rotation-time"
:invalid-feedback=
"$options.i18n.fields.startsOn.error"
:state=
"validationState.startsOn"
>
<div
class=
"gl-display-flex gl-align-items-center"
>
<gl-datepicker
v-model=
"form.startsOn.date"
class=
"gl-mr-3"
@
close=
"validateForm('startsOn')"
/>
<span>
{{ __('at') }}
</span>
<gl-dropdown
id=
"rotation-time"
:text=
"formatTime(form.startsOn.time)"
class=
"gl-w-12 gl-pl-3"
>
<gl-dropdown-item
v-for=
"n in 24"
:key=
"n"
:is-checked=
"form.startsOn.time === n"
is-check-item
@
click=
"setRotationStartsOnTime(n)"
>
<span
class=
"gl-white-space-nowrap"
>
{{ formatTime(n) }}
</span>
</gl-dropdown-item>
</gl-dropdown>
<span
class=
"gl-pl-5"
>
{{ schedule.timezone }}
</span>
</div>
</gl-form-group>
</gl-form>
</gl-modal>
</template>
ee/app/assets/javascripts/oncall_schedules/components/schedule/components/rotations_list_section.vue
View file @
54420c4f
<
script
>
<
script
>
import
{
GlButtonGroup
,
GlButton
,
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
{
GlButtonGroup
,
GlButton
,
GlTooltipDirective
,
GlModalDirective
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
s__
}
from
'
~/locale
'
;
import
CurrentDayIndicator
from
'
./current_day_indicator.vue
'
;
import
CurrentDayIndicator
from
'
./current_day_indicator.vue
'
;
import
RotationAssignee
from
'
../../rotations/components/rotation_assignee.vue
'
;
import
RotationAssignee
from
'
../../rotations/components/rotation_assignee.vue
'
;
import
{
editRotationModalId
}
from
'
../../../constants
'
;
export
const
i18n
=
{
export
const
i18n
=
{
editRotationLabel
:
s__
(
'
OnCallSchedules|Edit rotation
'
),
editRotationLabel
:
s__
(
'
OnCallSchedules|Edit rotation
'
),
...
@@ -11,6 +12,7 @@ export const i18n = {
...
@@ -11,6 +12,7 @@ export const i18n = {
export
default
{
export
default
{
i18n
,
i18n
,
editRotationModalId
,
components
:
{
components
:
{
GlButtonGroup
,
GlButtonGroup
,
GlButton
,
GlButton
,
...
@@ -18,6 +20,7 @@ export default {
...
@@ -18,6 +20,7 @@ export default {
RotationAssignee
,
RotationAssignee
,
},
},
directives
:
{
directives
:
{
GlModal
:
GlModalDirective
,
GlTooltip
:
GlTooltipDirective
,
GlTooltip
:
GlTooltipDirective
,
},
},
props
:
{
props
:
{
...
@@ -50,6 +53,7 @@ export default {
...
@@ -50,6 +53,7 @@ export default {
<span
class=
"gl-str-truncated"
>
{{
rotation
.
name
}}
</span>
<span
class=
"gl-str-truncated"
>
{{
rotation
.
name
}}
</span>
<gl-button-group
class=
"gl-px-2"
>
<gl-button-group
class=
"gl-px-2"
>
<gl-button
<gl-button
v-gl-modal=
"$options.editRotationModalId"
v-gl-tooltip
v-gl-tooltip
category=
"tertiary"
category=
"tertiary"
:title=
"$options.i18n.editRotationLabel"
:title=
"$options.i18n.editRotationLabel"
...
@@ -57,6 +61,7 @@ export default {
...
@@ -57,6 +61,7 @@ export default {
:aria-label=
"$options.i18n.editRotationLabel"
:aria-label=
"$options.i18n.editRotationLabel"
/>
/>
<gl-button
<gl-button
v-gl-modal=
"$options.editRotationModalId"
v-gl-tooltip
v-gl-tooltip
category=
"tertiary"
category=
"tertiary"
:title=
"$options.i18n.deleteRotationLabel"
:title=
"$options.i18n.deleteRotationLabel"
...
...
ee/app/assets/javascripts/oncall_schedules/constants.js
View file @
54420c4f
export
const
LENGTH_ENUM
=
{
export
const
LENGTH_ENUM
=
{
hours
:
'
hours
'
,
hours
:
'
HOURS
'
,
days
:
'
days
'
,
days
:
'
DAYS
'
,
weeks
:
'
weeks
'
,
weeks
:
'
WEEKS
'
,
};
};
export
const
CHEVRON_SKIPPING_SHADE_ENUM
=
[
'
500
'
,
'
600
'
,
'
700
'
,
'
800
'
,
'
900
'
,
'
950
'
];
export
const
CHEVRON_SKIPPING_SHADE_ENUM
=
[
'
500
'
,
'
600
'
,
'
700
'
,
'
800
'
,
'
900
'
,
'
950
'
];
...
@@ -9,6 +9,7 @@ export const CHEVRON_SKIPPING_SHADE_ENUM = ['500', '600', '700', '800', '900', '
...
@@ -9,6 +9,7 @@ export const CHEVRON_SKIPPING_SHADE_ENUM = ['500', '600', '700', '800', '900', '
export
const
CHEVRON_SKIPPING_PALETTE_ENUM
=
[
'
blue
'
,
'
orange
'
,
'
aqua
'
,
'
green
'
,
'
magenta
'
];
export
const
CHEVRON_SKIPPING_PALETTE_ENUM
=
[
'
blue
'
,
'
orange
'
,
'
aqua
'
,
'
green
'
,
'
magenta
'
];
export
const
DAYS_IN_WEEK
=
7
;
export
const
DAYS_IN_WEEK
=
7
;
export
const
HOURS_IN_DAY
=
24
;
export
const
PRESET_TYPES
=
{
export
const
PRESET_TYPES
=
{
WEEKS
:
'
WEEKS
'
,
WEEKS
:
'
WEEKS
'
,
...
@@ -19,3 +20,6 @@ export const PRESET_DEFAULTS = {
...
@@ -19,3 +20,6 @@ export const PRESET_DEFAULTS = {
TIMEFRAME_LENGTH
:
2
,
TIMEFRAME_LENGTH
:
2
,
},
},
};
};
export
const
addRotationModalId
=
'
addRotationModal
'
;
export
const
editRotationModalId
=
'
editRotationModal
'
;
ee/app/assets/javascripts/oncall_schedules/graphql/create_oncall_schedule_rotation.mutation.graphql
deleted
100644 → 0
View file @
1f25b49d
#import "~/graphql_shared/fragments/user.fragment.graphql"
mutation
oncallScheduleRotationCreate
(
$oncallScheduleRotationCreateInput
:
OncallScheduleRotationCreateInput
!
)
{
oncallScheduleRotationCreate
(
input
:
$oncallScheduleRotationCreateInput
)
{
errors
oncallScheduleRotation
{
iid
name
participants
{
nodes
{
...
User
}
}
length
{
value
type
}
startsOn
{
date
time
}
}
}
}
ee/app/assets/javascripts/oncall_schedules/graphql/fragments/oncall_schedule_rotation.fragment.graphql
0 → 100644
View file @
54420c4f
fragment
OnCallRotation
on
IncidentManagementOncallRotation
{
id
name
startsAt
length
lengthUnit
participants
{
nodes
{
user
{
id
username
}
colorWeight
colorPalette
}
}
}
ee/app/assets/javascripts/oncall_schedules/graphql/mutations/create_oncall_schedule_rotation.mutation.graphql
0 → 100644
View file @
54420c4f
#import "../fragments/oncall_schedule_rotation.fragment.graphql"
mutation
newRotation
(
$OncallRotationCreateInput
:
OncallRotationCreateInput
!)
{
oncallRotationCreate
(
input
:
$OncallRotationCreateInput
)
{
errors
oncallRotation
{
...
OnCallRotation
}
}
}
ee/app/assets/javascripts/oncall_schedules/graphql/mutations/update_oncall_schedule_rotation.mutation.graphql
0 → 100644
View file @
54420c4f
#import "../fragments/oncall_schedule_rotation.fragment.graphql"
mutation
updateRotation
(
$OncallRotationUpdateInput
:
OncallRotationUpdateInput
!)
{
oncallRotationUpdate
(
input
:
$OncallRotationUpdateInput
)
{
errors
oncallRotation
{
...
OnCallRotation
}
}
}
ee/app/assets/javascripts/oncall_schedules/utils/cache_updates.js
View file @
54420c4f
import
produce
from
'
immer
'
;
import
produce
from
'
immer
'
;
import
createFlash
from
'
~/flash
'
;
import
createFlash
from
'
~/flash
'
;
import
{
DELETE_SCHEDULE_ERROR
,
UPDATE_SCHEDULE_ERROR
}
from
'
./error_messages
'
;
import
{
DELETE_SCHEDULE_ERROR
,
UPDATE_SCHEDULE_ERROR
,
UPDATE_ROTATION_ERROR
,
}
from
'
./error_messages
'
;
const
addScheduleToStore
=
(
store
,
query
,
{
oncallSchedule
:
schedule
},
variables
)
=>
{
const
addScheduleToStore
=
(
store
,
query
,
{
oncallSchedule
:
schedule
},
variables
)
=>
{
if
(
!
schedule
)
{
if
(
!
schedule
)
{
...
@@ -75,6 +79,65 @@ const updateScheduleFromStore = (store, query, { oncallScheduleUpdate }, variabl
...
@@ -75,6 +79,65 @@ const updateScheduleFromStore = (store, query, { oncallScheduleUpdate }, variabl
});
});
};
};
const
addRotationToStore
=
(
store
,
query
,
{
oncallRotationCreate
:
rotation
},
scheduleId
,
variables
,
)
=>
{
if
(
!
rotation
)
{
return
;
}
const
sourceData
=
store
.
readQuery
({
query
,
variables
,
});
// TODO: This needs the rotation backend to be fully integrated to work, for the moment we will place-hold it.
const
data
=
produce
(
sourceData
,
(
draftData
)
=>
{
const
rotations
=
[
rotation
];
// eslint-disable-next-line no-param-reassign
draftData
.
project
.
incidentManagementOncallSchedules
.
nodes
.
find
(
({
iid
})
=>
iid
===
scheduleId
,
).
rotations
=
rotations
;
});
store
.
writeQuery
({
query
,
variables
,
data
,
});
};
const
updateRotationFromStore
=
(
store
,
query
,
{
oncallRotationUpdate
},
scheduleId
,
variables
)
=>
{
const
rotation
=
oncallRotationUpdate
?.
oncallRotation
;
if
(
!
rotation
)
{
return
;
}
const
sourceData
=
store
.
readQuery
({
query
,
variables
,
});
const
data
=
produce
(
sourceData
,
(
draftData
)
=>
{
// eslint-disable-next-line no-param-reassign
draftData
.
project
.
incidentManagementOncallSchedules
.
nodes
=
[
...
draftData
.
project
.
incidentManagementOncallSchedules
.
nodes
,
rotation
,
];
});
store
.
writeQuery
({
query
,
variables
,
data
,
});
};
const
onError
=
(
data
,
message
)
=>
{
const
onError
=
(
data
,
message
)
=>
{
createFlash
({
message
});
createFlash
({
message
});
throw
new
Error
(
data
.
errors
);
throw
new
Error
(
data
.
errors
);
...
@@ -103,3 +166,17 @@ export const updateStoreAfterScheduleEdit = (store, query, data, variables) => {
...
@@ -103,3 +166,17 @@ export const updateStoreAfterScheduleEdit = (store, query, data, variables) => {
updateScheduleFromStore
(
store
,
query
,
data
,
variables
);
updateScheduleFromStore
(
store
,
query
,
data
,
variables
);
}
}
};
};
export
const
updateStoreAfterRotationAdd
=
(
store
,
query
,
data
,
scheduleId
,
variables
)
=>
{
if
(
!
hasErrors
(
data
))
{
addRotationToStore
(
store
,
query
,
data
,
scheduleId
,
variables
);
}
};
export
const
updateStoreAfterRotationEdit
=
(
store
,
query
,
data
,
scheduleId
,
variables
)
=>
{
if
(
hasErrors
(
data
))
{
onError
(
data
,
UPDATE_ROTATION_ERROR
);
}
else
{
updateRotationFromStore
(
store
,
query
,
data
,
scheduleId
,
variables
);
}
};
ee/app/assets/javascripts/oncall_schedules/utils/error_messages.js
View file @
54420c4f
...
@@ -7,3 +7,7 @@ export const DELETE_SCHEDULE_ERROR = s__(
...
@@ -7,3 +7,7 @@ export const DELETE_SCHEDULE_ERROR = s__(
export
const
UPDATE_SCHEDULE_ERROR
=
s__
(
export
const
UPDATE_SCHEDULE_ERROR
=
s__
(
'
OnCallSchedules|The schedule could not be updated. Please try again.
'
,
'
OnCallSchedules|The schedule could not be updated. Please try again.
'
,
);
);
export
const
UPDATE_ROTATION_ERROR
=
s__
(
'
OnCallSchedules|The rotation could not be updated. Please try again.
'
,
);
ee/spec/frontend/oncall_schedule/mocks/apollo_mock.js
View file @
54420c4f
...
@@ -109,3 +109,59 @@ export const newlyCreatedSchedule = {
...
@@ -109,3 +109,59 @@ export const newlyCreatedSchedule = {
name
:
'
S-Monitor rotations
'
,
name
:
'
S-Monitor rotations
'
,
timezone
:
'
Kyiv/EST
'
,
timezone
:
'
Kyiv/EST
'
,
};
};
export
const
createRotationResponse
=
{
data
:
{
oncallRotationCreate
:
{
errors
:
[],
oncallRotation
:
{
id
:
'
37
'
,
name
:
'
Test
'
,
startsAt
:
'
2020-12-17T12:00:00Z
'
,
length
:
5
,
lengthUnit
:
'
WEEKS
'
,
participants
:
{
nodes
:
[
{
user
:
{
id
:
'
gid://gitlab/User/50
'
,
username
:
'
project_1_bot3
'
,
__typename
:
'
User
'
},
colorWeight
:
'
500
'
,
colorPalette
:
'
blue
'
,
__typename
:
'
OncallParticipantType
'
,
},
],
__typename
:
'
OncallParticipantTypeConnection
'
,
},
__typename
:
'
IncidentManagementOncallRotation
'
,
},
__typename
:
'
OncallRotationCreatePayload
'
,
},
},
};
export
const
createRotationResponseWithErrors
=
{
data
:
{
oncallRotationCreate
:
{
errors
:
[
'
Houston, we have a problem
'
],
oncallRotation
:
{
id
:
'
37
'
,
name
:
'
Test
'
,
startsAt
:
'
2020-12-17T12:00:00Z
'
,
length
:
5
,
lengthUnit
:
'
WEEKS
'
,
participants
:
{
nodes
:
[
{
user
:
{
id
:
'
gid://gitlab/User/50
'
,
username
:
'
project_1_bot3
'
,
__typename
:
'
User
'
},
colorWeight
:
'
500
'
,
colorPalette
:
'
blue
'
,
__typename
:
'
OncallParticipantType
'
,
},
],
__typename
:
'
OncallParticipantTypeConnection
'
,
},
__typename
:
'
IncidentManagementOncallRotation
'
,
},
__typename
:
'
OncallRotationCreatePayload
'
,
},
},
};
ee/spec/frontend/oncall_schedule/rotations/components/__snapshots__/add_edit_rotation_modal_spec.js.snap
0 → 100644
View file @
54420c4f
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddEditRotationModal renders rotation modal layout 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
modalclass=""
modalid="addRotationModal"
size="sm"
title="Add rotation"
titletag="h4"
>
<!---->
<add-edit-rotation-form-stub
form="[object Object]"
participants=""
schedule="[object Object]"
/>
</gl-modal-stub>
`;
ee/spec/frontend/oncall_schedule/rotations/components/__snapshots__/add_rotation_modal_spec.js.snap
deleted
100644 → 0
View file @
1f25b49d
This diff is collapsed.
Click to expand it.
ee/spec/frontend/oncall_schedule/rotations/components/add_edit_rotation_form_spec.js
0 → 100644
View file @
54420c4f
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
GlDropdownItem
,
GlTokenSelector
}
from
'
@gitlab/ui
'
;
import
AddEditRotationForm
from
'
ee/oncall_schedules/components/rotations/components/add_edit_rotation_form.vue
'
;
import
{
LENGTH_ENUM
}
from
'
ee/oncall_schedules/constants
'
;
import
{
participants
,
getOncallSchedulesQueryResponse
}
from
'
../../mocks/apollo_mock
'
;
const
projectPath
=
'
group/project
'
;
const
schedule
=
getOncallSchedulesQueryResponse
.
data
.
project
.
incidentManagementOncallSchedules
.
nodes
[
0
];
describe
(
'
AddEditRotationForm
'
,
()
=>
{
let
wrapper
;
const
createComponent
=
({
data
=
{},
props
=
{}
}
=
{})
=>
{
wrapper
=
shallowMount
(
AddEditRotationForm
,
{
data
()
{
return
{
...
data
,
};
},
propsData
:
{
...
props
,
schedule
,
isLoading
:
false
,
rotationNameIsValid
:
true
,
rotationParticipantsAreValid
:
true
,
rotationStartsAtIsValid
:
true
,
participants
,
form
:
{
name
:
''
,
participants
:
[],
rotationLength
:
{
length
:
1
,
unit
:
LENGTH_ENUM
.
hours
,
},
startsAt
:
{
date
:
null
,
time
:
0
,
},
},
},
provide
:
{
projectPath
,
},
});
};
beforeEach
(()
=>
{
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
const
findRotationLength
=
()
=>
wrapper
.
find
(
'
[id = "rotation-length"]
'
);
const
findRotationStartsOn
=
()
=>
wrapper
.
find
(
'
[id = "rotation-time"]
'
);
const
findUserSelector
=
()
=>
wrapper
.
find
(
GlTokenSelector
);
const
findDropdownOptions
=
()
=>
wrapper
.
findAll
(
GlDropdownItem
);
describe
(
'
Rotation length and start time
'
,
()
=>
{
it
(
'
renders the rotation length value
'
,
async
()
=>
{
const
rotationLength
=
findRotationLength
();
expect
(
rotationLength
.
exists
()).
toBe
(
true
);
expect
(
rotationLength
.
attributes
(
'
value
'
)).
toBe
(
'
1
'
);
});
it
(
'
renders the rotation starts on datepicker
'
,
async
()
=>
{
const
startsOn
=
findRotationStartsOn
();
expect
(
startsOn
.
exists
()).
toBe
(
true
);
expect
(
startsOn
.
attributes
(
'
text
'
)).
toBe
(
'
00:00
'
);
expect
(
startsOn
.
attributes
(
'
headertext
'
)).
toBe
(
''
);
});
it
(
'
should add a check for a rotation length type selected
'
,
async
()
=>
{
const
selectedLengthType1
=
findDropdownOptions
().
at
(
0
);
const
selectedLengthType2
=
findDropdownOptions
().
at
(
1
);
selectedLengthType1
.
vm
.
$emit
(
'
click
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
selectedLengthType1
.
props
(
'
isChecked
'
)).
toBe
(
true
);
expect
(
selectedLengthType2
.
props
(
'
isChecked
'
)).
toBe
(
false
);
});
});
describe
(
'
filter participants
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
(
'
has user options that are populated via apollo
'
,
()
=>
{
expect
(
findUserSelector
().
props
(
'
dropdownItems
'
)).
toHaveLength
(
participants
.
length
);
});
it
(
'
calls the API and sets dropdown items as request result
'
,
async
()
=>
{
const
tokenSelector
=
findUserSelector
();
tokenSelector
.
vm
.
$emit
(
'
focus
'
);
tokenSelector
.
vm
.
$emit
(
'
blur
'
);
tokenSelector
.
vm
.
$emit
(
'
focus
'
);
await
waitForPromises
();
expect
(
tokenSelector
.
props
(
'
dropdownItems
'
)).
toMatchObject
(
participants
);
expect
(
tokenSelector
.
props
(
'
hideDropdownWithNoItems
'
)).
toBe
(
false
);
});
it
(
'
emits `input` event with selected users
'
,
()
=>
{
findUserSelector
().
vm
.
$emit
(
'
input
'
,
participants
);
expect
(
findUserSelector
().
emitted
().
input
[
0
][
0
]).
toEqual
(
participants
);
});
it
(
'
when text input is blurred the text input clears
'
,
async
()
=>
{
const
tokenSelector
=
findUserSelector
();
tokenSelector
.
vm
.
$emit
(
'
blur
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
tokenSelector
.
props
(
'
hideDropdownWithNoItems
'
)).
toBe
(
false
);
});
});
});
ee/spec/frontend/oncall_schedule/rotations/components/add_rotation_modal_spec.js
→
ee/spec/frontend/oncall_schedule/rotations/components/add_
edit_
rotation_modal_spec.js
View file @
54420c4f
...
@@ -2,12 +2,23 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
...
@@ -2,12 +2,23 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import
createMockApollo
from
'
jest/helpers/mock_apollo_helper
'
;
import
createMockApollo
from
'
jest/helpers/mock_apollo_helper
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
GlDropdownItem
,
GlModal
,
GlAlert
,
GlTokenSelector
}
from
'
@gitlab/ui
'
;
import
{
GlModal
,
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
addRotationModalId
}
from
'
ee/oncall_schedules/components/oncall_schedule
'
;
import
{
addRotationModalId
}
from
'
ee/oncall_schedules/constants
'
;
import
AddRotationModal
from
'
ee/oncall_schedules/components/rotations/components/add_rotation_modal.vue
'
;
import
AddEditRotationModal
,
{
// import createOncallScheduleRotationMutation from 'ee/oncall_schedules/graphql/create_oncall_schedule_rotation.mutation.graphql';
i18n
,
}
from
'
ee/oncall_schedules/components/rotations/components/add_edit_rotation_modal.vue
'
;
import
getOncallSchedulesQuery
from
'
ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql
'
;
import
createOncallScheduleRotationMutation
from
'
ee/oncall_schedules/graphql/mutations/create_oncall_schedule_rotation.mutation.graphql
'
;
import
createFlash
,
{
FLASH_TYPES
}
from
'
~/flash
'
;
import
usersSearchQuery
from
'
~/graphql_shared/queries/users_search.query.graphql
'
;
import
usersSearchQuery
from
'
~/graphql_shared/queries/users_search.query.graphql
'
;
import
{
getOncallSchedulesQueryResponse
,
participants
}
from
'
../../mocks/apollo_mock
'
;
import
{
participants
,
getOncallSchedulesQueryResponse
,
createRotationResponse
,
createRotationResponseWithErrors
,
}
from
'
../../mocks/apollo_mock
'
;
jest
.
mock
(
'
~/flash
'
);
const
schedule
=
const
schedule
=
getOncallSchedulesQueryResponse
.
data
.
project
.
incidentManagementOncallSchedules
.
nodes
[
0
];
getOncallSchedulesQueryResponse
.
data
.
project
.
incidentManagementOncallSchedules
.
nodes
[
0
];
...
@@ -16,12 +27,11 @@ const projectPath = 'group/project';
...
@@ -16,12 +27,11 @@ const projectPath = 'group/project';
const
mutate
=
jest
.
fn
();
const
mutate
=
jest
.
fn
();
const
mockHideModal
=
jest
.
fn
();
const
mockHideModal
=
jest
.
fn
();
localVue
.
use
(
VueApollo
);
describe
(
'
AddEditRotationModal
'
,
()
=>
{
describe
(
'
AddRotationModal
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
let
fakeApollo
;
let
fakeApollo
;
let
userSearchQueryHandler
;
let
userSearchQueryHandler
;
let
createRotationHandler
;
async
function
awaitApolloDomMock
()
{
async
function
awaitApolloDomMock
()
{
await
wrapper
.
vm
.
$nextTick
();
// kick off the DOM update
await
wrapper
.
vm
.
$nextTick
();
// kick off the DOM update
...
@@ -29,8 +39,12 @@ describe('AddRotationModal', () => {
...
@@ -29,8 +39,12 @@ describe('AddRotationModal', () => {
await
wrapper
.
vm
.
$nextTick
();
// kick off the DOM update for flash
await
wrapper
.
vm
.
$nextTick
();
// kick off the DOM update for flash
}
}
async
function
createRotation
(
localWrapper
)
{
localWrapper
.
find
(
GlModal
).
vm
.
$emit
(
'
primary
'
,
{
preventDefault
:
jest
.
fn
()
});
}
const
createComponent
=
({
data
=
{},
props
=
{},
loading
=
false
}
=
{})
=>
{
const
createComponent
=
({
data
=
{},
props
=
{},
loading
=
false
}
=
{})
=>
{
wrapper
=
shallowMount
(
AddRotationModal
,
{
wrapper
=
shallowMount
(
Add
Edit
RotationModal
,
{
data
()
{
data
()
{
return
{
return
{
...
data
,
...
data
,
...
@@ -55,13 +69,31 @@ describe('AddRotationModal', () => {
...
@@ -55,13 +69,31 @@ describe('AddRotationModal', () => {
},
},
},
},
});
});
wrapper
.
vm
.
$refs
.
create
ScheduleRotationModal
.
hide
=
mockHideModal
;
wrapper
.
vm
.
$refs
.
addEdit
ScheduleRotationModal
.
hide
=
mockHideModal
;
};
};
const
createComponentWithApollo
=
({
search
=
''
}
=
{})
=>
{
const
createComponentWithApollo
=
({
fakeApollo
=
createMockApollo
([[
usersSearchQuery
,
userSearchQueryHandler
]]);
search
=
''
,
createHandler
=
jest
.
fn
().
mockResolvedValue
(
createRotationResponse
),
}
=
{})
=>
{
createRotationHandler
=
createHandler
;
localVue
.
use
(
VueApollo
);
fakeApollo
=
createMockApollo
([
[
getOncallSchedulesQuery
,
jest
.
fn
().
mockResolvedValue
(
getOncallSchedulesQueryResponse
)],
[
usersSearchQuery
,
userSearchQueryHandler
],
[
createOncallScheduleRotationMutation
,
createRotationHandler
],
]);
fakeApollo
.
clients
.
defaultClient
.
cache
.
writeQuery
({
query
:
getOncallSchedulesQuery
,
variables
:
{
projectPath
:
'
group/project
'
,
},
data
:
getOncallSchedulesQueryResponse
.
data
,
});
wrapper
=
shallowMount
(
AddRotationModal
,
{
wrapper
=
shallowMount
(
Add
Edit
RotationModal
,
{
localVue
,
localVue
,
propsData
:
{
propsData
:
{
modalId
:
addRotationModalId
,
modalId
:
addRotationModalId
,
...
@@ -81,6 +113,8 @@ describe('AddRotationModal', () => {
...
@@ -81,6 +113,8 @@ describe('AddRotationModal', () => {
projectPath
,
projectPath
,
},
},
});
});
wrapper
.
vm
.
$refs
.
addEditScheduleRotationModal
.
hide
=
mockHideModal
;
};
};
beforeEach
(()
=>
{
beforeEach
(()
=>
{
...
@@ -93,91 +127,26 @@ describe('AddRotationModal', () => {
...
@@ -93,91 +127,26 @@ describe('AddRotationModal', () => {
});
});
const
findModal
=
()
=>
wrapper
.
find
(
GlModal
);
const
findModal
=
()
=>
wrapper
.
find
(
GlModal
);
const
findRotationLength
=
()
=>
wrapper
.
find
(
'
[id = "rotation-length"]
'
);
const
findRotationStartsOn
=
()
=>
wrapper
.
find
(
'
[id = "rotation-time"]
'
);
const
findUserSelector
=
()
=>
wrapper
.
find
(
GlTokenSelector
);
const
findDropdownOptions
=
()
=>
wrapper
.
findAll
(
GlDropdownItem
);
const
findAlert
=
()
=>
wrapper
.
find
(
GlAlert
);
const
findAlert
=
()
=>
wrapper
.
find
(
GlAlert
);
it
(
'
renders rotation modal layout
'
,
()
=>
{
it
(
'
renders rotation modal layout
'
,
()
=>
{
expect
(
wrapper
.
element
).
toMatchSnapshot
();
expect
(
wrapper
.
element
).
toMatchSnapshot
();
});
});
describe
(
'
Rotation length and start time
'
,
()
=>
{
it
(
'
renders the rotation length value
'
,
async
()
=>
{
const
rotationLength
=
findRotationLength
();
expect
(
rotationLength
.
exists
()).
toBe
(
true
);
expect
(
rotationLength
.
attributes
(
'
value
'
)).
toBe
(
'
1
'
);
});
it
(
'
renders the rotation starts on datepicker
'
,
async
()
=>
{
const
startsOn
=
findRotationStartsOn
();
expect
(
startsOn
.
exists
()).
toBe
(
true
);
expect
(
startsOn
.
attributes
(
'
text
'
)).
toBe
(
'
00:00
'
);
expect
(
startsOn
.
attributes
(
'
headertext
'
)).
toBe
(
''
);
});
it
(
'
should add a check for a rotation length type selected
'
,
async
()
=>
{
const
selectedLengthType1
=
findDropdownOptions
().
at
(
0
);
const
selectedLengthType2
=
findDropdownOptions
().
at
(
1
);
selectedLengthType1
.
vm
.
$emit
(
'
click
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
selectedLengthType1
.
props
(
'
isChecked
'
)).
toBe
(
true
);
expect
(
selectedLengthType2
.
props
(
'
isChecked
'
)).
toBe
(
false
);
});
});
describe
(
'
filter participants
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
data
:
{
participants
}
});
});
it
(
'
has user options that are populated via apollo
'
,
()
=>
{
expect
(
findUserSelector
().
props
(
'
dropdownItems
'
).
length
).
toBe
(
participants
.
length
);
});
it
(
'
calls the API and sets dropdown items as request result
'
,
async
()
=>
{
const
tokenSelector
=
findUserSelector
();
tokenSelector
.
vm
.
$emit
(
'
focus
'
);
tokenSelector
.
vm
.
$emit
(
'
blur
'
);
tokenSelector
.
vm
.
$emit
(
'
focus
'
);
await
waitForPromises
();
expect
(
tokenSelector
.
props
(
'
dropdownItems
'
)).
toMatchObject
(
participants
);
expect
(
tokenSelector
.
props
(
'
hideDropdownWithNoItems
'
)).
toBe
(
false
);
});
it
(
'
emits `input` event with selected users
'
,
()
=>
{
findUserSelector
().
vm
.
$emit
(
'
input
'
,
participants
);
expect
(
findUserSelector
().
emitted
().
input
[
0
][
0
]).
toEqual
(
participants
);
});
it
(
'
when text input is blurred the text input clears
'
,
async
()
=>
{
const
tokenSelector
=
findUserSelector
();
tokenSelector
.
vm
.
$emit
(
'
blur
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
tokenSelector
.
props
(
'
hideDropdownWithNoItems
'
)).
toBe
(
false
);
});
});
describe
(
'
Rotation create
'
,
()
=>
{
describe
(
'
Rotation create
'
,
()
=>
{
it
(
'
makes a request with `oncall
Schedule
RotationCreate` to create a schedule rotation
'
,
()
=>
{
it
(
'
makes a request with `oncallRotationCreate` to create a schedule rotation
'
,
()
=>
{
mutate
.
mockResolvedValueOnce
({});
mutate
.
mockResolvedValueOnce
({});
findModal
().
vm
.
$emit
(
'
primary
'
,
{
preventDefault
:
jest
.
fn
()
});
findModal
().
vm
.
$emit
(
'
primary
'
,
{
preventDefault
:
jest
.
fn
()
});
expect
(
mutate
).
toHaveBeenCalledWith
({
expect
(
mutate
).
toHaveBeenCalledWith
({
mutation
:
expect
.
any
(
Object
),
mutation
:
expect
.
any
(
Object
),
variables
:
{
oncallScheduleRotationCreate
:
expect
.
objectContaining
({
projectPath
})
},
update
:
expect
.
anything
(),
variables
:
{
OncallRotationCreateInput
:
expect
.
objectContaining
({
projectPath
})
},
});
});
});
});
it
(
'
does not hide the rotation modal and shows error alert on fail
'
,
async
()
=>
{
it
(
'
does not hide the rotation modal and shows error alert on fail
'
,
async
()
=>
{
const
error
=
'
some error
'
;
const
error
=
'
some error
'
;
mutate
.
mockResolvedValueOnce
({
data
:
{
oncall
Schedule
RotationCreate
:
{
errors
:
[
error
]
}
}
});
mutate
.
mockResolvedValueOnce
({
data
:
{
oncallRotationCreate
:
{
errors
:
[
error
]
}
}
});
findModal
().
vm
.
$emit
(
'
primary
'
,
{
preventDefault
:
jest
.
fn
()
});
findModal
().
vm
.
$emit
(
'
primary
'
,
{
preventDefault
:
jest
.
fn
()
});
await
waitForPromises
();
await
waitForPromises
();
expect
(
mockHideModal
).
not
.
toHaveBeenCalled
();
expect
(
mockHideModal
).
not
.
toHaveBeenCalled
();
...
@@ -200,6 +169,31 @@ describe('AddRotationModal', () => {
...
@@ -200,6 +169,31 @@ describe('AddRotationModal', () => {
expect
(
userSearchQueryHandler
).
toHaveBeenCalledWith
({
search
:
'
root
'
});
expect
(
userSearchQueryHandler
).
toHaveBeenCalledWith
({
search
:
'
root
'
});
});
});
// TODO: Once the BE is complete for the mutation add specs here for that via a creationHandler
it
(
'
calls a mutation with correct parameters and creates a rotation
'
,
async
()
=>
{
createComponentWithApollo
();
await
createRotation
(
wrapper
);
await
awaitApolloDomMock
();
expect
(
mockHideModal
).
toHaveBeenCalled
();
expect
(
createRotationHandler
).
toHaveBeenCalled
();
expect
(
createFlash
).
toHaveBeenCalledWith
({
message
:
i18n
.
rotationCreated
,
type
:
FLASH_TYPES
.
SUCCESS
,
});
});
it
(
'
displays alert if mutation had a recoverable error
'
,
async
()
=>
{
createComponentWithApollo
({
createHandler
:
jest
.
fn
().
mockResolvedValue
(
createRotationResponseWithErrors
),
});
await
createRotation
(
wrapper
);
await
awaitApolloDomMock
();
const
alert
=
findAlert
();
expect
(
alert
.
exists
()).
toBe
(
true
);
expect
(
alert
.
text
()).
toContain
(
'
Houston, we have a problem
'
);
});
});
});
});
});
ee/spec/frontend/oncall_schedule/schedule/components/__snapshots__/rotations_list_section_spec.js.snap
View file @
54420c4f
...
@@ -24,7 +24,9 @@ exports[`RotationsListSectionComponent renders component layout 1`] = `
...
@@ -24,7 +24,9 @@ exports[`RotationsListSectionComponent renders component layout 1`] = `
buttontextclasses=""
buttontextclasses=""
category="tertiary"
category="tertiary"
icon="pencil"
icon="pencil"
role="button"
size="medium"
size="medium"
tabindex="0"
title="Edit rotation"
title="Edit rotation"
variant="default"
variant="default"
/>
/>
...
@@ -34,7 +36,9 @@ exports[`RotationsListSectionComponent renders component layout 1`] = `
...
@@ -34,7 +36,9 @@ exports[`RotationsListSectionComponent renders component layout 1`] = `
buttontextclasses=""
buttontextclasses=""
category="tertiary"
category="tertiary"
icon="remove"
icon="remove"
role="button"
size="medium"
size="medium"
tabindex="0"
title="Delete rotation"
title="Delete rotation"
variant="default"
variant="default"
/>
/>
...
...
ee/spec/frontend/oncall_schedule/schedule/components/schedule_timeline_section_spec.js
View file @
54420c4f
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlCard
}
from
'
@gitlab/ui
'
;
import
ScheduleTimelineSection
from
'
ee/oncall_schedules/components/schedule/components/schedule_timeline_section.vue
'
;
import
ScheduleTimelineSection
from
'
ee/oncall_schedules/components/schedule/components/schedule_timeline_section.vue
'
;
import
WeeksHeaderItem
from
'
ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_item.vue
'
;
import
WeeksHeaderItem
from
'
ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_item.vue
'
;
import
{
getTimeframeForWeeksView
}
from
'
ee/oncall_schedules/components/schedule/utils
'
;
import
{
getTimeframeForWeeksView
}
from
'
ee/oncall_schedules/components/schedule/utils
'
;
import
{
PRESET_TYPES
}
from
'
ee/oncall_schedules/constants
'
;
import
{
PRESET_TYPES
}
from
'
ee/oncall_schedules/constants
'
;
import
{
getOncallSchedulesQueryResponse
}
from
'
../../mocks/apollo_mock
'
;
describe
(
'
TimelineSectionComponent
'
,
()
=>
{
describe
(
'
TimelineSectionComponent
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
const
mockTimeframeInitialDate
=
new
Date
(
2018
,
0
,
1
);
const
mockTimeframeInitialDate
=
new
Date
(
2018
,
0
,
1
);
const
mockTimeframeWeeks
=
getTimeframeForWeeksView
(
mockTimeframeInitialDate
);
const
mockTimeframeWeeks
=
getTimeframeForWeeksView
(
mockTimeframeInitialDate
);
const
schedule
=
getOncallSchedulesQueryResponse
.
data
.
project
.
incidentManagementOncallSchedules
.
nodes
[
0
];
function
mountComponent
({
function
mountComponent
({
presetType
=
PRESET_TYPES
.
WEEKS
,
presetType
=
PRESET_TYPES
.
WEEKS
,
...
@@ -18,9 +20,7 @@ describe('TimelineSectionComponent', () => {
...
@@ -18,9 +20,7 @@ describe('TimelineSectionComponent', () => {
propsData
:
{
propsData
:
{
presetType
,
presetType
,
timeframe
,
timeframe
,
},
schedule
,
stubs
:
{
GlCard
,
},
},
});
});
}
}
...
@@ -30,10 +30,8 @@ describe('TimelineSectionComponent', () => {
...
@@ -30,10 +30,8 @@ describe('TimelineSectionComponent', () => {
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
if
(
wrapper
)
{
wrapper
.
destroy
();
wrapper
.
destroy
();
wrapper
=
null
;
wrapper
=
null
;
}
});
});
it
(
'
renders component container element with class `timeline-section`
'
,
()
=>
{
it
(
'
renders component container element with class `timeline-section`
'
,
()
=>
{
...
...
locale/gitlab.pot
View file @
54420c4f
...
@@ -19535,6 +19535,15 @@ msgstr ""
...
@@ -19535,6 +19535,15 @@ msgstr ""
msgid "OnCallSchedules|Sets the default timezone for the schedule, for all participants"
msgid "OnCallSchedules|Sets the default timezone for the schedule, for all participants"
msgstr ""
msgstr ""
msgid "OnCallSchedules|Successfully created a new rotation"
msgstr ""
msgid "OnCallSchedules|Successfully edited your rotation"
msgstr ""
msgid "OnCallSchedules|The rotation could not be updated. Please try again."
msgstr ""
msgid "OnCallSchedules|The schedule could not be deleted. Please try again."
msgid "OnCallSchedules|The schedule could not be deleted. Please try again."
msgstr ""
msgstr ""
...
...
spec/frontend/lib/utils/datetime_utility_spec.js
View file @
54420c4f
...
@@ -731,3 +731,28 @@ describe('datesMatch', () => {
...
@@ -731,3 +731,28 @@ describe('datesMatch', () => {
expect
(
datetimeUtility
.
datesMatch
(
date1
,
date2
)).
toBe
(
expected
);
expect
(
datetimeUtility
.
datesMatch
(
date1
,
date2
)).
toBe
(
expected
);
});
});
});
});
describe
(
'
format24HourTimeStringFromInt
'
,
()
=>
{
const
expectedFormattedTimes
=
[
[
0
,
'
00:00
'
],
[
2
,
'
02:00
'
],
[
6
,
'
06:00
'
],
[
9
,
'
09:00
'
],
[
10
,
'
10:00
'
],
[
16
,
'
16:00
'
],
[
22
,
'
22:00
'
],
[
32
,
''
],
[
NaN
,
''
],
[
'
Invalid Int
'
,
''
],
[
null
,
''
],
[
undefined
,
''
],
];
expectedFormattedTimes
.
forEach
(([
timeInt
,
expectedTimeStringIn24HourNotation
])
=>
{
it
(
`formats
${
timeInt
}
as
${
expectedTimeStringIn24HourNotation
}
`
,
()
=>
{
expect
(
datetimeUtility
.
format24HourTimeStringFromInt
(
timeInt
)).
toBe
(
expectedTimeStringIn24HourNotation
,
);
});
});
});
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