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
eacecc62
Commit
eacecc62
authored
Jan 05, 2021
by
David O'Regan
Committed by
Kushal Pandya
Jan 05, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Enable Rotation creation
Enable rotation creation in on call schedules via GraphQL with supporting specs
parent
d4ed85ad
Changes
22
Hide 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 @
eacecc62
...
...
@@ -806,3 +806,20 @@ export const dateAtFirstDayOfMonth = (date) => new Date(newDate(date).setDate(1)
* @return {Boolean} true if the dates match
*/
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 @
eacecc62
...
...
@@ -12,11 +12,10 @@ import { s__, __ } from '~/locale';
import
ScheduleTimelineSection
from
'
./schedule/components/schedule_timeline_section.vue
'
;
import
DeleteScheduleModal
from
'
./delete_schedule_modal.vue
'
;
import
EditScheduleModal
from
'
./add_edit_schedule_modal.vue
'
;
import
AddRotationModal
from
'
./rotations/components/add_rotation_modal.vue
'
;
import
{
getTimeframeForWeeksView
}
from
'
./schedule/utils
'
;
import
{
PRESET_TYPES
}
from
'
../constants
'
;
import
AddEditRotationModal
from
'
./rotations/components/add_edit_rotation_modal.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
=
{
scheduleForTz
:
s__
(
'
OnCallSchedules|On-call schedule for the %{timezone}
'
),
...
...
@@ -25,14 +24,13 @@ export const i18n = {
rotationTitle
:
s__
(
'
OnCallSchedules|Rotations
'
),
addARotation
:
s__
(
'
OnCallSchedules|Add a rotation
'
),
};
export
const
addRotationModalId
=
'
addRotationModal
'
;
export
const
editScheduleModalId
=
'
editScheduleModal
'
;
export
const
deleteScheduleModalId
=
'
deleteScheduleModal
'
;
export
default
{
i18n
,
addRotationModalId
,
editRotationModalId
,
editScheduleModalId
,
deleteScheduleModalId
,
presetType
:
PRESET_TYPES
.
WEEKS
,
...
...
@@ -45,7 +43,7 @@ export default {
GlButton
,
DeleteScheduleModal
,
EditScheduleModal
,
AddRotationModal
,
Add
Edit
RotationModal
,
RotationsListSection
,
},
directives
:
{
...
...
@@ -151,6 +149,11 @@ export default {
:modal-id=
"$options.editScheduleModalId"
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>
</template>
ee/app/assets/javascripts/oncall_schedules/components/rotations/components/add_edit_rotation_form.vue
0 → 100644
View file @
eacecc62
<
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 @
eacecc62
<
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 @
d4ed85ad
<
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 @
eacecc62
<
script
>
import
{
GlButtonGroup
,
GlButton
,
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
{
GlButtonGroup
,
GlButton
,
GlTooltipDirective
,
GlModalDirective
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
~/locale
'
;
import
CurrentDayIndicator
from
'
./current_day_indicator.vue
'
;
import
RotationAssignee
from
'
../../rotations/components/rotation_assignee.vue
'
;
import
{
editRotationModalId
}
from
'
../../../constants
'
;
export
const
i18n
=
{
editRotationLabel
:
s__
(
'
OnCallSchedules|Edit rotation
'
),
...
...
@@ -11,6 +12,7 @@ export const i18n = {
export
default
{
i18n
,
editRotationModalId
,
components
:
{
GlButtonGroup
,
GlButton
,
...
...
@@ -18,6 +20,7 @@ export default {
RotationAssignee
,
},
directives
:
{
GlModal
:
GlModalDirective
,
GlTooltip
:
GlTooltipDirective
,
},
props
:
{
...
...
@@ -50,6 +53,7 @@ export default {
<span
class=
"gl-str-truncated"
>
{{
rotation
.
name
}}
</span>
<gl-button-group
class=
"gl-px-2"
>
<gl-button
v-gl-modal=
"$options.editRotationModalId"
v-gl-tooltip
category=
"tertiary"
:title=
"$options.i18n.editRotationLabel"
...
...
@@ -57,6 +61,7 @@ export default {
:aria-label=
"$options.i18n.editRotationLabel"
/>
<gl-button
v-gl-modal=
"$options.editRotationModalId"
v-gl-tooltip
category=
"tertiary"
:title=
"$options.i18n.deleteRotationLabel"
...
...
ee/app/assets/javascripts/oncall_schedules/constants.js
View file @
eacecc62
export
const
LENGTH_ENUM
=
{
hours
:
'
hours
'
,
days
:
'
days
'
,
weeks
:
'
weeks
'
,
hours
:
'
HOURS
'
,
days
:
'
DAYS
'
,
weeks
:
'
WEEKS
'
,
};
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', '
export
const
CHEVRON_SKIPPING_PALETTE_ENUM
=
[
'
blue
'
,
'
orange
'
,
'
aqua
'
,
'
green
'
,
'
magenta
'
];
export
const
DAYS_IN_WEEK
=
7
;
export
const
HOURS_IN_DAY
=
24
;
export
const
PRESET_TYPES
=
{
WEEKS
:
'
WEEKS
'
,
...
...
@@ -19,3 +20,6 @@ export const PRESET_DEFAULTS = {
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 @
d4ed85ad
#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 @
eacecc62
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 @
eacecc62
#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 @
eacecc62
#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 @
eacecc62
import
produce
from
'
immer
'
;
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
)
=>
{
if
(
!
schedule
)
{
...
...
@@ -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
)
=>
{
createFlash
({
message
});
throw
new
Error
(
data
.
errors
);
...
...
@@ -103,3 +166,17 @@ export const updateStoreAfterScheduleEdit = (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 @
eacecc62
...
...
@@ -7,3 +7,7 @@ export const DELETE_SCHEDULE_ERROR = s__(
export
const
UPDATE_SCHEDULE_ERROR
=
s__
(
'
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 @
eacecc62
...
...
@@ -109,3 +109,59 @@ export const newlyCreatedSchedule = {
name
:
'
S-Monitor rotations
'
,
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 @
eacecc62
// 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 @
d4ed85ad
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddRotationModal renders rotation modal layout 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
modalclass=""
modalid="addRotationModal"
size="sm"
title="Add rotation"
titletag="h4"
>
<!---->
<gl-form-stub
class="w-75 gl-xs-w-full!"
>
<gl-form-group-stub
invalid-feedback="Rotation name cannot be empty"
label="Name"
label-for="rotation-name"
label-size="sm"
state="true"
>
<gl-form-input-stub
id="rotation-name"
value=""
/>
</gl-form-group-stub>
<gl-form-group-stub
invalid-feedback="Rotation participants cannot be empty"
label="Participants"
label-for="rotation-participants"
label-size="sm"
state="true"
>
<gl-token-selector-stub
autocomplete="off"
containerclass="gl-h-13! gl-overflow-y-auto"
dropdownitems=""
selectedtokens=""
/>
</gl-form-group-stub>
<gl-form-group-stub
label="Rotation length"
label-for="rotation-length"
label-size="sm"
>
<div
class="gl-display-flex"
>
<gl-form-input-stub
class="gl-w-12 gl-mr-3"
id="rotation-length"
min="1"
type="number"
value="1"
/>
<gl-dropdown-stub
category="primary"
headertext=""
id="rotation-length"
size="medium"
text="hours"
variant="default"
>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischecked="true"
ischeckitem="true"
secondarytext=""
>
hours
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
days
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
weeks
</gl-dropdown-item-stub>
</gl-dropdown-stub>
</div>
</gl-form-group-stub>
<gl-form-group-stub
invalid-feedback="Rotation start date cannot be empty"
label="Starts on"
label-for="rotation-time"
label-size="sm"
state="true"
>
<div
class="gl-display-flex gl-align-items-center"
>
<gl-datepicker-stub
ariallabel=""
autocomplete=""
class="gl-mr-3"
container=""
displayfield="true"
firstday="0"
placeholder="YYYY-MM-DD"
target=""
theme=""
/>
<span>
at
</span>
<gl-dropdown-stub
category="primary"
class="gl-w-12 gl-pl-3"
headertext=""
id="rotation-time"
size="medium"
text="00:00"
variant="default"
>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
01:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
02:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
03:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
04:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
05:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
06:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
07:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
08:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
09:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
10:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
11:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
12:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
13:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
14:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
15:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
16:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
17:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
18:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
19:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
20:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
21:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
22:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
23:00
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
24:00
</span>
</gl-dropdown-item-stub>
</gl-dropdown-stub>
<span
class="gl-pl-5"
>
{
"identifier": "Pacific/Honolulu"
}
</span>
</div>
</gl-form-group-stub>
</gl-form-stub>
</gl-modal-stub>
`;
ee/spec/frontend/oncall_schedule/rotations/components/add_edit_rotation_form_spec.js
0 → 100644
View file @
eacecc62
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 @
eacecc62
...
...
@@ -2,12 +2,23 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import
createMockApollo
from
'
jest/helpers/mock_apollo_helper
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
GlDropdownItem
,
GlModal
,
GlAlert
,
GlTokenSelector
}
from
'
@gitlab/ui
'
;
import
{
addRotationModalId
}
from
'
ee/oncall_schedules/components/oncall_schedule
'
;
import
AddRotationModal
from
'
ee/oncall_schedules/components/rotations/components/add_rotation_modal.vue
'
;
// import createOncallScheduleRotationMutation from 'ee/oncall_schedules/graphql/create_oncall_schedule_rotation.mutation.graphql';
import
{
GlModal
,
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
addRotationModalId
}
from
'
ee/oncall_schedules/constants
'
;
import
AddEditRotationModal
,
{
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
{
getOncallSchedulesQueryResponse
,
participants
}
from
'
../../mocks/apollo_mock
'
;
import
{
participants
,
getOncallSchedulesQueryResponse
,
createRotationResponse
,
createRotationResponseWithErrors
,
}
from
'
../../mocks/apollo_mock
'
;
jest
.
mock
(
'
~/flash
'
);
const
schedule
=
getOncallSchedulesQueryResponse
.
data
.
project
.
incidentManagementOncallSchedules
.
nodes
[
0
];
...
...
@@ -16,12 +27,11 @@ const projectPath = 'group/project';
const
mutate
=
jest
.
fn
();
const
mockHideModal
=
jest
.
fn
();
localVue
.
use
(
VueApollo
);
describe
(
'
AddRotationModal
'
,
()
=>
{
describe
(
'
AddEditRotationModal
'
,
()
=>
{
let
wrapper
;
let
fakeApollo
;
let
userSearchQueryHandler
;
let
createRotationHandler
;
async
function
awaitApolloDomMock
()
{
await
wrapper
.
vm
.
$nextTick
();
// kick off the DOM update
...
...
@@ -29,8 +39,12 @@ describe('AddRotationModal', () => {
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
}
=
{})
=>
{
wrapper
=
shallowMount
(
AddRotationModal
,
{
wrapper
=
shallowMount
(
Add
Edit
RotationModal
,
{
data
()
{
return
{
...
data
,
...
...
@@ -55,13 +69,31 @@ describe('AddRotationModal', () => {
},
},
});
wrapper
.
vm
.
$refs
.
create
ScheduleRotationModal
.
hide
=
mockHideModal
;
wrapper
.
vm
.
$refs
.
addEdit
ScheduleRotationModal
.
hide
=
mockHideModal
;
};
const
createComponentWithApollo
=
({
search
=
''
}
=
{})
=>
{
fakeApollo
=
createMockApollo
([[
usersSearchQuery
,
userSearchQueryHandler
]]);
const
createComponentWithApollo
=
({
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
,
propsData
:
{
modalId
:
addRotationModalId
,
...
...
@@ -81,6 +113,8 @@ describe('AddRotationModal', () => {
projectPath
,
},
});
wrapper
.
vm
.
$refs
.
addEditScheduleRotationModal
.
hide
=
mockHideModal
;
};
beforeEach
(()
=>
{
...
...
@@ -93,91 +127,26 @@ describe('AddRotationModal', () => {
});
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
);
it
(
'
renders rotation modal layout
'
,
()
=>
{
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
'
,
()
=>
{
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
({});
findModal
().
vm
.
$emit
(
'
primary
'
,
{
preventDefault
:
jest
.
fn
()
});
expect
(
mutate
).
toHaveBeenCalledWith
({
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
()
=>
{
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
()
});
await
waitForPromises
();
expect
(
mockHideModal
).
not
.
toHaveBeenCalled
();
...
...
@@ -200,6 +169,31 @@ describe('AddRotationModal', () => {
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 @
eacecc62
...
...
@@ -24,7 +24,9 @@ exports[`RotationsListSectionComponent renders component layout 1`] = `
buttontextclasses=""
category="tertiary"
icon="pencil"
role="button"
size="medium"
tabindex="0"
title="Edit rotation"
variant="default"
/>
...
...
@@ -34,7 +36,9 @@ exports[`RotationsListSectionComponent renders component layout 1`] = `
buttontextclasses=""
category="tertiary"
icon="remove"
role="button"
size="medium"
tabindex="0"
title="Delete rotation"
variant="default"
/>
...
...
ee/spec/frontend/oncall_schedule/schedule/components/schedule_timeline_section_spec.js
View file @
eacecc62
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
WeeksHeaderItem
from
'
ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_item.vue
'
;
import
{
getTimeframeForWeeksView
}
from
'
ee/oncall_schedules/components/schedule/utils
'
;
import
{
PRESET_TYPES
}
from
'
ee/oncall_schedules/constants
'
;
import
{
getOncallSchedulesQueryResponse
}
from
'
../../mocks/apollo_mock
'
;
describe
(
'
TimelineSectionComponent
'
,
()
=>
{
let
wrapper
;
const
mockTimeframeInitialDate
=
new
Date
(
2018
,
0
,
1
);
const
mockTimeframeWeeks
=
getTimeframeForWeeksView
(
mockTimeframeInitialDate
);
const
schedule
=
getOncallSchedulesQueryResponse
.
data
.
project
.
incidentManagementOncallSchedules
.
nodes
[
0
];
function
mountComponent
({
presetType
=
PRESET_TYPES
.
WEEKS
,
...
...
@@ -18,9 +20,7 @@ describe('TimelineSectionComponent', () => {
propsData
:
{
presetType
,
timeframe
,
},
stubs
:
{
GlCard
,
schedule
,
},
});
}
...
...
@@ -30,10 +30,8 @@ describe('TimelineSectionComponent', () => {
});
afterEach
(()
=>
{
if
(
wrapper
)
{
wrapper
.
destroy
();
wrapper
=
null
;
}
wrapper
.
destroy
();
wrapper
=
null
;
});
it
(
'
renders component container element with class `timeline-section`
'
,
()
=>
{
...
...
locale/gitlab.pot
View file @
eacecc62
...
...
@@ -19535,6 +19535,15 @@ msgstr ""
msgid "OnCallSchedules|Sets the default timezone for the schedule, for all participants"
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."
msgstr ""
...
...
spec/frontend/lib/utils/datetime_utility_spec.js
View file @
eacecc62
...
...
@@ -731,3 +731,28 @@ describe('datesMatch', () => {
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