Commit 2995faf9 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch '262850-delete-schedule-dor' into 'master'

Allow delete of oncall schedule

See merge request gitlab-org/gitlab!48909
parents 932b1603 dc8f04ed
<script>
import { isEqual, isEmpty } from 'lodash';
import {
GlForm,
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
export const i18n = {
selectTimezone: s__('OnCallSchedules|Select timezone'),
search: __('Search'),
noResults: __('No matching results'),
fields: {
name: {
title: __('Name'),
validation: {
empty: __("Can't be empty"),
},
},
description: { title: __('Description (optional)') },
timezone: {
title: __('Timezone'),
description: s__(
'OnCallSchedules|Sets the default timezone for the schedule, for all participants',
),
validation: {
empty: __("Can't be empty"),
},
},
},
errorMsg: s__('OnCallSchedules|Failed to add schedule'),
};
export default {
i18n,
inject: ['projectPath', 'timezones'],
components: {
GlForm,
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
},
props: {
form: {
type: Object,
required: true,
},
isNameInvalid: {
type: Boolean,
required: true,
},
isTimezoneInvalid: {
type: Boolean,
required: true,
},
schedule: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
tzSearchTerm: '',
};
},
computed: {
filteredTimezones() {
const lowerCaseTzSearchTerm = this.tzSearchTerm.toLowerCase();
return this.timezones.filter(tz =>
this.getFormattedTimezone(tz)
.toLowerCase()
.includes(lowerCaseTzSearchTerm),
);
},
noResults() {
return !this.filteredTimezones.length;
},
selectedTimezone() {
return isEmpty(this.form.timezone)
? i18n.selectTimezone
: this.getFormattedTimezone(this.form.timezone);
},
},
methods: {
getFormattedTimezone(tz) {
return __(`(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`);
},
isTimezoneSelected(tz) {
return isEqual(tz, this.form.timezone);
},
},
};
</script>
<template>
<gl-form>
<gl-form-group
:label="$options.i18n.fields.name.title"
:invalid-feedback="$options.i18n.fields.name.validation.empty"
label-size="sm"
label-for="schedule-name"
>
<gl-form-input
id="schedule-name"
:value="form.name"
:state="!isNameInvalid"
@input="$emit('update-schedule-form', { type: 'name', value: $event })"
/>
</gl-form-group>
<gl-form-group
:label="$options.i18n.fields.description.title"
label-size="sm"
label-for="schedule-description"
>
<gl-form-input
id="schedule-description"
:value="form.description"
@input="$emit('update-schedule-form', { type: 'description', value: $event })"
/>
</gl-form-group>
<gl-form-group
:label="$options.i18n.fields.timezone.title"
label-size="sm"
label-for="schedule-timezone"
:description="$options.i18n.fields.timezone.description"
:state="!isTimezoneInvalid"
:invalid-feedback="$options.i18n.fields.timezone.validation.empty"
>
<gl-dropdown
id="schedule-timezone"
:text="selectedTimezone"
class="timezone-dropdown gl-w-full"
:header-text="$options.i18n.selectTimezone"
:class="{ 'invalid-dropdown': isTimezoneInvalid }"
>
<gl-search-box-by-type v-model.trim="tzSearchTerm" />
<gl-dropdown-item
v-for="tz in filteredTimezones"
:key="getFormattedTimezone(tz)"
:is-checked="isTimezoneSelected(tz)"
is-check-item
@click="$emit('update-schedule-form', { type: 'timezone', value: tz })"
>
<span class="gl-white-space-nowrap"> {{ getFormattedTimezone(tz) }}</span>
</gl-dropdown-item>
<gl-dropdown-item v-if="noResults">
{{ $options.i18n.noResults }}
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
</gl-form>
</template>
<script> <script>
import { isEqual, isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { import { GlModal, GlAlert } from '@gitlab/ui';
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlAlert,
} from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import createOncallScheduleMutation from '../graphql/create_oncall_schedule.mutation.graphql'; import createOncallScheduleMutation from '../graphql/mutations/create_oncall_schedule.mutation.graphql';
import { getFormattedTimezone } from '../utils'; import AddEditScheduleForm from './add_edit_schedule_form.vue';
export const i18n = { export const i18n = {
selectTimezone: s__('OnCallSchedules|Select timezone'),
search: __('Search'),
noResults: __('No matching results'),
cancel: __('Cancel'), cancel: __('Cancel'),
addSchedule: s__('OnCallSchedules|Add schedule'), addSchedule: s__('OnCallSchedules|Add schedule'),
fields: {
name: {
title: __('Name'),
validation: {
empty: __("Can't be empty"),
},
},
description: { title: __('Description (optional)') },
timezone: {
title: __('Timezone'),
description: s__(
'OnCallSchedules|Sets the default timezone for the schedule, for all participants',
),
validation: {
empty: __("Can't be empty"),
},
},
},
errorMsg: s__('OnCallSchedules|Failed to add schedule'), errorMsg: s__('OnCallSchedules|Failed to add schedule'),
}; };
...@@ -46,13 +16,8 @@ export default { ...@@ -46,13 +16,8 @@ export default {
inject: ['projectPath', 'timezones'], inject: ['projectPath', 'timezones'],
components: { components: {
GlModal, GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlAlert, GlAlert,
AddEditScheduleForm,
}, },
props: { props: {
modalId: { modalId: {
...@@ -63,11 +28,10 @@ export default { ...@@ -63,11 +28,10 @@ export default {
data() { data() {
return { return {
loading: false, loading: false,
tzSearchTerm: '',
form: { form: {
name: '', name: '',
description: '', description: '',
timezone: {}, timezone: '',
}, },
error: null, error: null,
}; };
...@@ -88,22 +52,6 @@ export default { ...@@ -88,22 +52,6 @@ export default {
}, },
}; };
}, },
filteredTimezones() {
const lowerCaseTzSearchTerm = this.tzSearchTerm.toLowerCase();
return this.timezones.filter(tz =>
this.getFormattedTimezone(tz)
.toLowerCase()
.includes(lowerCaseTzSearchTerm),
);
},
noResults() {
return !this.filteredTimezones.length;
},
selectedTimezone() {
return isEmpty(this.form.timezone)
? i18n.selectTimezone
: this.getFormattedTimezone(this.form.timezone);
},
isNameInvalid() { isNameInvalid() {
return !this.form.name.length; return !this.form.name.length;
}, },
...@@ -142,18 +90,12 @@ export default { ...@@ -142,18 +90,12 @@ export default {
this.loading = false; this.loading = false;
}); });
}, },
setSelectedTimezone(tz) {
this.form.timezone = tz;
},
getFormattedTimezone(tz) {
return getFormattedTimezone(tz);
},
isTimezoneSelected(tz) {
return isEqual(tz, this.form.timezone);
},
hideErrorAlert() { hideErrorAlert() {
this.error = null; this.error = null;
}, },
updateScheduleForm({ type, value }) {
this.form[type] = value;
},
}, },
}; };
</script> </script>
...@@ -171,54 +113,11 @@ export default { ...@@ -171,54 +113,11 @@ export default {
<gl-alert v-if="error" variant="danger" class="gl-mt-n3 gl-mb-3" @dismiss="hideErrorAlert"> <gl-alert v-if="error" variant="danger" class="gl-mt-n3 gl-mb-3" @dismiss="hideErrorAlert">
{{ error || $options.i18n.errorMsg }} {{ error || $options.i18n.errorMsg }}
</gl-alert> </gl-alert>
<gl-form> <add-edit-schedule-form
<gl-form-group :is-name-invalid="isNameInvalid"
:label="$options.i18n.fields.name.title" :is-timezone-invalid="isTimezoneInvalid"
:invalid-feedback="$options.i18n.fields.name.validation.empty" :form="form"
label-size="sm" @update-schedule-form="updateScheduleForm"
label-for="schedule-name" />
>
<gl-form-input id="schedule-name" v-model="form.name" :state="!isNameInvalid" />
</gl-form-group>
<gl-form-group
:label="$options.i18n.fields.description.title"
label-size="sm"
label-for="schedule-description"
>
<gl-form-input id="schedule-description" v-model="form.description" />
</gl-form-group>
<gl-form-group
:label="$options.i18n.fields.timezone.title"
label-size="sm"
label-for="schedule-timezone"
:description="$options.i18n.fields.timezone.description"
:state="!isTimezoneInvalid"
:invalid-feedback="$options.i18n.fields.timezone.validation.empty"
>
<gl-dropdown
id="schedule-timezone"
:text="selectedTimezone"
class="timezone-dropdown gl-w-full"
:header-text="$options.i18n.selectTimezone"
:class="{ 'invalid-dropdown': isTimezoneInvalid }"
>
<gl-search-box-by-type v-model.trim="tzSearchTerm" />
<gl-dropdown-item
v-for="tz in filteredTimezones"
:key="getFormattedTimezone(tz)"
:is-checked="isTimezoneSelected(tz)"
is-check-item
@click="setSelectedTimezone(tz)"
>
<span class="gl-white-space-nowrap"> {{ getFormattedTimezone(tz) }}</span>
</gl-dropdown-item>
<gl-dropdown-item v-if="noResults">
{{ $options.i18n.noResults }}
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
</gl-form>
</gl-modal> </gl-modal>
</template> </template>
<script>
import { GlSprintf, GlModal, GlAlert } from '@gitlab/ui';
import destroyOncallScheduleMutation from '../graphql/mutations/destroy_oncall_schedule.mutation.graphql';
import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import { updateStoreAfterScheduleDelete } from '../utils/cache_updates';
import { s__, __ } from '~/locale';
export const i18n = {
deleteSchedule: s__('OnCallSchedules|Delete schedule'),
deleteScheduleMessage: s__(
'OnCallSchedules|Are you sure you want to delete the "%{deleteSchedule}" schedule? This action cannot be undone.',
),
};
export default {
i18n,
components: {
GlSprintf,
GlModal,
GlAlert,
},
inject: ['projectPath'],
props: {
schedule: {
type: Object,
required: true,
},
},
data() {
return {
loading: false,
error: null,
};
},
computed: {
primaryProps() {
return {
text: this.$options.i18n.deleteSchedule,
attributes: [{ category: 'primary' }, { variant: 'danger' }, { loading: this.loading }],
};
},
cancelProps() {
return {
text: __('Cancel'),
};
},
},
methods: {
deleteSchedule() {
const { projectPath } = this;
this.loading = true;
this.$apollo
.mutate({
mutation: destroyOncallScheduleMutation,
variables: {
id: this.schedule.id,
projectPath,
},
update(store, { data }) {
updateStoreAfterScheduleDelete(store, getOncallSchedulesQuery, data, { projectPath });
},
})
.then(({ data: { oncallScheduleDestroy } = {} } = {}) => {
const error = oncallScheduleDestroy.errors[0];
if (error) {
throw error;
}
this.$refs.deleteScheduleModal.hide();
})
.catch(error => {
this.error = error;
})
.finally(() => {
this.loading = false;
});
},
hideErrorAlert() {
this.error = null;
},
},
};
</script>
<template>
<gl-modal
ref="deleteScheduleModal"
modal-id="deleteScheduleModal"
size="sm"
:title="$options.i18n.deleteSchedule"
:action-primary="primaryProps"
:action-cancel="cancelProps"
@primary.prevent="deleteSchedule"
>
<gl-alert v-if="error" variant="danger" class="gl-mt-n3 gl-mb-3" @dismiss="hideErrorAlert">
{{ error || $options.i18n.errorMsg }}
</gl-alert>
<gl-sprintf :message="$options.i18n.deleteScheduleMessage">
<template #deleteSchedule>{{ schedule.name }}</template>
</gl-sprintf>
</gl-modal>
</template>
<script>
import { isEmpty } from 'lodash';
import { GlModal, GlAlert } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import updateOncallScheduleMutation from '../graphql/mutations/update_oncall_schedule.mutation.graphql';
import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import { updateStoreAfterScheduleEdit } from '../utils/cache_updates';
import AddEditScheduleForm from './add_edit_schedule_form.vue';
export const i18n = {
cancel: __('Cancel'),
editSchedule: s__('OnCallSchedules|Edit schedule'),
errorMsg: s__('OnCallSchedules|Failed to edit schedule'),
};
export default {
i18n,
inject: ['projectPath', 'timezones'],
components: {
GlModal,
GlAlert,
AddEditScheduleForm,
},
props: {
schedule: {
type: Object,
required: true,
},
},
data() {
return {
loading: false,
error: null,
form: {
name: this.schedule.name,
description: this.schedule.description,
timezone: this.timezones.find(({ identifier }) => this.schedule.timezone === identifier),
},
};
},
computed: {
actionsProps() {
return {
primary: {
text: i18n.editSchedule,
attributes: [
{ variant: 'info' },
{ loading: this.loading },
{ disabled: this.isFormInvalid },
],
},
cancel: {
text: i18n.cancel,
},
};
},
isNameInvalid() {
return !this.form.name.length;
},
isTimezoneInvalid() {
return isEmpty(this.form.timezone);
},
isFormInvalid() {
return this.isNameInvalid || this.isTimezoneInvalid;
},
editScheduleVariables() {
return {
projectPath: this.projectPath,
...this.form,
timezone: this.form.timezone.identifier,
};
},
},
methods: {
editSchedule() {
const { projectPath } = this;
this.loading = true;
this.$apollo
.mutate({
mutation: updateOncallScheduleMutation,
variables: {
oncallScheduleEditInput: this.editScheduleVariables,
},
update(store, { data }) {
updateStoreAfterScheduleEdit(store, getOncallSchedulesQuery, data, { projectPath });
},
})
.then(({ data: { oncallScheduleEdit: { errors: [error] } } }) => {
if (error) {
throw error;
}
this.$refs.updateScheduleModal.hide();
})
.catch(error => {
this.error = error;
})
.finally(() => {
this.loading = false;
});
},
hideErrorAlert() {
this.error = null;
},
updateScheduleForm({ type, value }) {
this.form[type] = value;
},
},
};
</script>
<template>
<gl-modal
ref="updateScheduleModal"
modal-id="updateScheduleModal"
size="sm"
:title="$options.i18n.editSchedule"
:action-primary="actionsProps.primary"
:action-cancel="actionsProps.cancel"
@primary.prevent="editSchedule"
>
<gl-alert v-if="error" variant="danger" class="gl-mt-n3 gl-mb-3" @dismiss="hideErrorAlert">
{{ error || $options.i18n.errorMsg }}
</gl-alert>
<add-edit-schedule-form
:is-name-invalid="isNameInvalid"
:is-timezone-invalid="isTimezoneInvalid"
:form="form"
:schedule="schedule"
@update-schedule-form="updateScheduleForm"
/>
</gl-modal>
</template>
<script> <script>
import { GlSprintf, GlCard } from '@gitlab/ui'; import { GlSprintf, GlCard, GlButtonGroup, GlButton, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale'; 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 EditScheduleModal from './edit_schedule_modal.vue';
import { getTimeframeForWeeksView } from './schedule/utils'; import { getTimeframeForWeeksView } from './schedule/utils';
import { PRESET_TYPES } from './schedule/constants'; import { PRESET_TYPES } from './schedule/constants';
import { getFormattedTimezone } from '../utils'; import { getFormattedTimezone } from '../utils';
...@@ -9,6 +11,8 @@ import { getFormattedTimezone } from '../utils'; ...@@ -9,6 +11,8 @@ import { getFormattedTimezone } from '../utils';
export const i18n = { export const i18n = {
title: s__('OnCallSchedules|On-call schedule'), title: s__('OnCallSchedules|On-call schedule'),
scheduleForTz: s__('OnCallSchedules|On-call schedule for the %{tzShort}'), scheduleForTz: s__('OnCallSchedules|On-call schedule for the %{tzShort}'),
updateScheduleLabel: s__('OnCallSchedules|Edit schedule'),
destroyScheduleLabel: s__('OnCallSchedules|Delete schedule'),
}; };
export default { export default {
...@@ -19,6 +23,13 @@ export default { ...@@ -19,6 +23,13 @@ export default {
GlSprintf, GlSprintf,
GlCard, GlCard,
ScheduleTimelineSection, ScheduleTimelineSection,
GlButtonGroup,
GlButton,
DeleteScheduleModal,
EditScheduleModal,
},
directives: {
GlModal: GlModalDirective,
}, },
props: { props: {
schedule: { schedule: {
...@@ -43,7 +54,21 @@ export default { ...@@ -43,7 +54,21 @@ export default {
<h2>{{ $options.i18n.title }}</h2> <h2>{{ $options.i18n.title }}</h2>
<gl-card> <gl-card>
<template #header> <template #header>
<h3 class="gl-font-weight-bold gl-font-lg gl-m-0">{{ schedule.name }}</h3> <div class="gl-display-flex gl-justify-content-space-between gl-m-0">
<span class="gl-font-weight-bold gl-font-lg">{{ schedule.name }}</span>
<gl-button-group>
<gl-button
v-gl-modal.updateScheduleModal
icon="pencil"
:aria-label="$options.i18n.updateScheduleLabel"
/>
<gl-button
v-gl-modal.deleteScheduleModal
icon="remove"
:aria-label="$options.i18n.destroyScheduleLabel"
/>
</gl-button-group>
</div>
</template> </template>
<p class="gl-text-gray-500 gl-mb-5"> <p class="gl-text-gray-500 gl-mb-5">
...@@ -57,5 +82,7 @@ export default { ...@@ -57,5 +82,7 @@ export default {
<schedule-timeline-section :preset-type="$options.presetType" :timeframe="timeframe" /> <schedule-timeline-section :preset-type="$options.presetType" :timeframe="timeframe" />
</div> </div>
</gl-card> </gl-card>
<delete-schedule-modal :schedule="schedule" />
<edit-schedule-modal :schedule="schedule" />
</div> </div>
</template> </template>
...@@ -2,10 +2,9 @@ ...@@ -2,10 +2,9 @@
import { GlEmptyState, GlButton, GlLoadingIcon, GlModalDirective } from '@gitlab/ui'; import { GlEmptyState, GlButton, GlLoadingIcon, GlModalDirective } from '@gitlab/ui';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
import AddScheduleModal from './add_schedule_modal.vue'; import AddScheduleModal from './add_schedule_modal.vue';
import AddRotationModal from './rotations/add_rotation_modal.vue';
import OncallSchedule from './oncall_schedule.vue'; import OncallSchedule from './oncall_schedule.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import getOncallSchedules from '../graphql/get_oncall_schedules.query.graphql'; import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
const addScheduleModalId = 'addScheduleModal'; const addScheduleModalId = 'addScheduleModal';
...@@ -27,7 +26,6 @@ export default { ...@@ -27,7 +26,6 @@ export default {
GlButton, GlButton,
GlLoadingIcon, GlLoadingIcon,
AddScheduleModal, AddScheduleModal,
AddRotationModal,
OncallSchedule, OncallSchedule,
}, },
directives: { directives: {
...@@ -41,7 +39,7 @@ export default { ...@@ -41,7 +39,7 @@ export default {
apollo: { apollo: {
schedule: { schedule: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
query: getOncallSchedules, query: getOncallSchedulesQuery,
variables() { variables() {
return { return {
projectPath: this.projectPath, projectPath: this.projectPath,
...@@ -77,12 +75,8 @@ export default { ...@@ -77,12 +75,8 @@ export default {
<gl-button v-gl-modal="$options.addScheduleModalId" variant="info"> <gl-button v-gl-modal="$options.addScheduleModalId" variant="info">
{{ $options.i18n.emptyState.button }} {{ $options.i18n.emptyState.button }}
</gl-button> </gl-button>
<gl-button v-gl-modal="'create-schedule-rotation-modal'" variant="danger">
{{ $options.i18n.emptyState.button }}
</gl-button>
</template> </template>
</gl-empty-state> </gl-empty-state>
<add-schedule-modal :modal-id="$options.addScheduleModalId" /> <add-schedule-modal :modal-id="$options.addScheduleModalId" />
<add-rotation-modal />
</div> </div>
</template> </template>
<script> <script>
import { GlCard, GlButton, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import WeeksHeaderItem from './preset_weeks/weeks_header_item.vue'; import WeeksHeaderItem from './preset_weeks/weeks_header_item.vue';
import AddRotationModal from '../../rotations/add_rotation_modal.vue';
export const i18n = {
rotationTitle: s__('OnCallSchedules|Rotations'),
addARotation: s__('OnCallSchedules|Add a rotation'),
};
export default { export default {
i18n,
components: { components: {
GlButton,
GlCard,
WeeksHeaderItem, WeeksHeaderItem,
AddRotationModal,
},
directives: {
GlModal: GlModalDirective,
}, },
props: { props: {
presetType: { presetType: {
...@@ -19,14 +34,28 @@ export default { ...@@ -19,14 +34,28 @@ export default {
</script> </script>
<template> <template>
<div class="timeline-section clearfix"> <div>
<span class="timeline-header-blank"></span> <gl-card header-class="gl-bg-transparent">
<weeks-header-item <template #header>
v-for="(timeframeItem, index) in timeframe" <div class="gl-display-flex gl-justify-content-space-between">
:key="index" <h6 class="gl-m-0">{{ $options.i18n.rotationTitle }}</h6>
:timeframe-index="index" <gl-button v-gl-modal="'create-schedule-rotation-modal'" variant="link">{{
:timeframe-item="timeframeItem" $options.i18n.addARotation
:timeframe="timeframe" }}</gl-button>
/> </div>
</template>
<div class="timeline-section clearfix">
<span class="timeline-header-blank"></span>
<weeks-header-item
v-for="(timeframeItem, index) in timeframe"
:key="index"
:timeframe-index="index"
:timeframe-item="timeframeItem"
:timeframe="timeframe"
/>
</div>
</gl-card>
<add-rotation-modal />
</div> </div>
</template> </template>
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export default new VueApollo({
defaultClient: createDefaultClient(
{},
{
cacheConfig: {},
assumeImmutableResults: true,
},
),
});
mutation oncallScheduleDestroy($oncallScheduleDestroyInput: OncallScheduleDestroyInput!) {
oncallScheduleDestroy(input: $oncallScheduleDestroyInput) {
errors
oncallSchedule {
iid
name
description
timezone
}
}
}
mutation oncallScheduleUpdate($oncallScheduleUpdateInput: oncallScheduleUpdateInput!) {
oncallScheduleUpdate(input: $oncallScheduleUpdateInput) {
errors
oncallSchedule {
iid
name
description
timezone
}
}
}
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import OnCallSchedulesWrapper from './components/oncall_schedules_wrapper.vue'; import OnCallSchedulesWrapper from './components/oncall_schedules_wrapper.vue';
import createDefaultClient from '~/lib/graphql'; import apolloProvider from './graphql';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -12,10 +12,6 @@ export default () => { ...@@ -12,10 +12,6 @@ export default () => {
const { projectPath, emptyOncallSchedulesSvgPath, timezones } = el.dataset; const { projectPath, emptyOncallSchedulesSvgPath, timezones } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({ return new Vue({
el, el,
apolloProvider, apolloProvider,
......
import produce from 'immer';
import createFlash from '~/flash';
import { DELETE_SCHEDULE_ERROR, UPDATE_SCHEDULE_ERROR } from './error_messages';
const deleteScheduleFromStore = (store, query, { oncallScheduleDestroy }, variables) => {
const schedule = oncallScheduleDestroy?.oncallSchedule;
if (!schedule) {
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.filter(
({ id }) => id !== schedule.id,
);
});
store.writeQuery({
query,
variables,
data,
});
};
const updateScheduleFromStore = (store, query, { oncallScheduleUpdate }, variables) => {
const schedule = oncallScheduleUpdate?.oncallSchedule;
if (!schedule) {
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,
schedule,
];
});
store.writeQuery({
query,
variables,
data,
});
};
const onError = (data, message) => {
createFlash({ message });
throw new Error(data.errors);
};
export const hasErrors = ({ errors = [] }) => errors?.length;
export const updateStoreAfterScheduleDelete = (store, query, data, variables) => {
if (hasErrors(data)) {
onError(data, DELETE_SCHEDULE_ERROR);
} else {
deleteScheduleFromStore(store, query, data, variables);
}
};
export const updateStoreAfterScheduleEdit = (store, query, data, variables) => {
if (hasErrors(data)) {
onError(data, UPDATE_SCHEDULE_ERROR);
} else {
updateScheduleFromStore(store, query, data, variables);
}
};
import { s__ } from '~/locale';
export const DELETE_SCHEDULE_ERROR = s__(
'OnCallSchedules|The schedule could not be deleted. Please try again.',
);
export const UPDATE_SCHEDULE_ERROR = s__(
'OnCallSchedules|The schedule could not be updated. Please try again.',
);
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddEditScheduleForm renders modal layout 1`] = `
<gl-form-stub
modalid="modalId"
>
<gl-form-group-stub
invalid-feedback="Can't be empty"
label="Name"
label-for="schedule-name"
label-size="sm"
>
<gl-form-input-stub
id="schedule-name"
state="true"
value="Test schedule"
/>
</gl-form-group-stub>
<gl-form-group-stub
label="Description (optional)"
label-for="schedule-description"
label-size="sm"
>
<gl-form-input-stub
id="schedule-description"
value="Description 1 lives here"
/>
</gl-form-group-stub>
<gl-form-group-stub
description="Sets the default timezone for the schedule, for all participants"
invalid-feedback="Can't be empty"
label="Timezone"
label-for="schedule-timezone"
label-size="sm"
state="true"
>
<gl-dropdown-stub
category="primary"
class="timezone-dropdown gl-w-full"
headertext="Select timezone"
id="schedule-timezone"
size="medium"
text="(UTC-12:00) -12 International Date Line West"
variant="default"
>
<gl-search-box-by-type-stub
clearbuttontitle="Clear"
value=""
/>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischecked="true"
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-12:00) -12 International Date Line West
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-11:00) SST American Samoa
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-11:00) SST Midway Island
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-10:00) HST Hawaii
</span>
</gl-dropdown-item-stub>
<!---->
</gl-dropdown-stub>
</gl-form-group-stub>
</gl-form-stub>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Add schedule modal renders modal layout 1`] = ` exports[`AddScheduleModal renders modal layout 1`] = `
<gl-modal-stub <gl-modal-stub
actioncancel="[object Object]" actioncancel="[object Object]"
actionprimary="[object Object]" actionprimary="[object Object]"
...@@ -12,115 +12,9 @@ exports[`Add schedule modal renders modal layout 1`] = ` ...@@ -12,115 +12,9 @@ exports[`Add schedule modal renders modal layout 1`] = `
> >
<!----> <!---->
<gl-form-stub> <add-edit-schedule-form-stub
<gl-form-group-stub form="[object Object]"
invalid-feedback="Can't be empty" schedule="[object Object]"
label="Name" />
label-for="schedule-name"
label-size="sm"
>
<gl-form-input-stub
id="schedule-name"
value=""
/>
</gl-form-group-stub>
<gl-form-group-stub
label="Description (optional)"
label-for="schedule-description"
label-size="sm"
>
<gl-form-input-stub
id="schedule-description"
value=""
/>
</gl-form-group-stub>
<gl-form-group-stub
description="Sets the default timezone for the schedule, for all participants"
invalid-feedback="Can't be empty"
label="Timezone"
label-for="schedule-timezone"
label-size="sm"
>
<gl-dropdown-stub
category="primary"
class="timezone-dropdown gl-w-full invalid-dropdown"
headertext="Select timezone"
id="schedule-timezone"
size="medium"
text="Select timezone"
variant="default"
>
<gl-search-box-by-type-stub
clearbuttontitle="Clear"
value=""
/>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-12:00) -12 International Date Line West
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-11:00) SST American Samoa
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-11:00) SST Midway Island
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-10:00) HST Hawaii
</span>
</gl-dropdown-item-stub>
<!---->
</gl-dropdown-stub>
</gl-form-group-stub>
</gl-form-stub>
</gl-modal-stub> </gl-modal-stub>
`; `;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DeleteScheduleModal renders delete schedule modal layout 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
modalclass=""
modalid="deleteScheduleModal"
size="sm"
title="Delete schedule"
titletag="h4"
>
<!---->
<gl-sprintf-stub
message="Are you sure you want to delete the \\"%{deleteSchedule}\\" schedule? This action cannot be undone."
/>
</gl-modal-stub>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UpdateScheduleModal renders update schedule modal layout 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
modalclass=""
modalid="updateScheduleModal"
size="sm"
title="Edit schedule"
titletag="h4"
>
<!---->
<add-edit-schedule-form-stub
form="[object Object]"
schedule="[object Object]"
/>
</gl-modal-stub>
`;
import { shallowMount } from '@vue/test-utils';
import { GlSearchBoxByType, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import AddEditScheduleForm, {
i18n,
} from 'ee/oncall_schedules/components/add_edit_schedule_form.vue';
import { getOncallSchedulesQueryResponse } from './mocks/apollo_mock';
import mockTimezones from './mocks/mockTimezones.json';
describe('AddEditScheduleForm', () => {
let wrapper;
const projectPath = 'group/project';
const mutate = jest.fn();
const mockSchedule =
getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0];
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(AddEditScheduleForm, {
propsData: {
modalId: 'modalId',
form: {
name: mockSchedule.name,
description: mockSchedule.description,
timezone: mockTimezones[0],
},
isNameInvalid: false,
isTimezoneInvalid: false,
schedule: mockSchedule,
...props,
},
provide: {
projectPath,
timezones: mockTimezones,
},
mocks: {
$apollo: {
mutate,
},
},
stubs: {
GlFormGroup: false,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findTimezoneDropdown = () => wrapper.find(GlDropdown);
const findDropdownOptions = () => wrapper.findAll(GlDropdownItem);
const findTimezoneSearchBox = () => wrapper.find(GlSearchBoxByType);
it('renders modal layout', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('Timezone select', () => {
it('has options based on provided BE data', () => {
expect(findDropdownOptions()).toHaveLength(mockTimezones.length);
});
it('formats each option', () => {
findDropdownOptions().wrappers.forEach((option, index) => {
const tz = mockTimezones[index];
const expectedValue = `(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`;
expect(option.text()).toBe(expectedValue);
});
});
describe('timezones filtering', () => {
it('should filter options based on search term', async () => {
const searchTerm = 'Hawaii';
findTimezoneSearchBox().vm.$emit('input', searchTerm);
await wrapper.vm.$nextTick();
const options = findDropdownOptions();
expect(options).toHaveLength(1);
expect(options.at(0).text()).toContain(searchTerm);
});
it('should display no results item when there are no filter matches', async () => {
const searchTerm = 'someUnexistentTZ';
findTimezoneSearchBox().vm.$emit('input', searchTerm);
await wrapper.vm.$nextTick();
const options = findDropdownOptions();
expect(options).toHaveLength(1);
expect(options.at(0).text()).toContain(i18n.noResults);
});
});
it('should add a checkmark to the selected option', async () => {
const selectedTZOption = findDropdownOptions().at(0);
selectedTZOption.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(selectedTZOption.attributes('ischecked')).toBe('true');
});
});
describe('Form validation', () => {
describe('Timezone select', () => {
it('has red border when nothing selected', () => {
createComponent({
props: {
schedule: null,
form: { name: '', description: '', timezone: '' },
isTimezoneInvalid: true,
},
});
expect(findTimezoneDropdown().classes()).toContain('invalid-dropdown');
});
it("doesn't have a red border when there is selected option", async () => {
findDropdownOptions()
.at(1)
.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(findTimezoneDropdown().classes()).not.toContain('invalid-dropdown');
});
});
});
});
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlSearchBoxByType, GlDropdown, GlDropdownItem, GlModal, GlAlert } from '@gitlab/ui'; import { GlModal, GlAlert } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import AddScheduleModal, { i18n } from 'ee/oncall_schedules/components/add_schedule_modal.vue'; import AddScheduleModal from 'ee/oncall_schedules/components/add_schedule_modal.vue';
import { getOncallSchedulesQueryResponse } from './mocks/apollo_mock';
import mockTimezones from './mocks/mockTimezones.json'; import mockTimezones from './mocks/mockTimezones.json';
describe('Add schedule modal', () => { describe('AddScheduleModal', () => {
let wrapper; let wrapper;
const projectPath = 'group/project'; const projectPath = 'group/project';
const mutate = jest.fn(); const mutate = jest.fn();
const mockHideModal = jest.fn(); const mockHideModal = jest.fn();
function mountComponent() { const createComponent = ({ data = {}, props = {} } = {}) => {
wrapper = shallowMount(AddScheduleModal, { wrapper = shallowMount(AddScheduleModal, {
data() {
return {
form:
getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0],
...data,
};
},
propsData: { propsData: {
modalId: 'modalId', modalId: 'modalId',
...props,
}, },
provide: { provide: {
projectPath, projectPath,
...@@ -24,17 +33,13 @@ describe('Add schedule modal', () => { ...@@ -24,17 +33,13 @@ describe('Add schedule modal', () => {
mutate, mutate,
}, },
}, },
stubs: {
GlFormGroup: false,
},
}); });
wrapper.vm.$refs.createScheduleModal.hide = mockHideModal; wrapper.vm.$refs.createScheduleModal.hide = mockHideModal;
} };
beforeEach(() => { beforeEach(() => {
mountComponent(); createComponent();
}); });
afterEach(() => { afterEach(() => {
...@@ -44,55 +49,11 @@ describe('Add schedule modal', () => { ...@@ -44,55 +49,11 @@ describe('Add schedule modal', () => {
const findModal = () => wrapper.find(GlModal); const findModal = () => wrapper.find(GlModal);
const findAlert = () => wrapper.find(GlAlert); const findAlert = () => wrapper.find(GlAlert);
const findTimezoneDropdown = () => wrapper.find(GlDropdown);
const findDropdownOptions = () => wrapper.findAll(GlDropdownItem);
const findTimezoneSearchBox = () => wrapper.find(GlSearchBoxByType);
it('renders modal layout', () => { it('renders modal layout', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
describe('Timezone select', () => {
it('has options based on provided BE data', () => {
expect(findDropdownOptions().length).toBe(mockTimezones.length);
});
it('formats each option', () => {
findDropdownOptions().wrappers.forEach((option, index) => {
const tz = mockTimezones[index];
const expectedValue = `(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`;
expect(option.text()).toBe(expectedValue);
});
});
describe('timezones filtering', () => {
it('should filter options based on search term', async () => {
const searchTerm = 'Hawaii';
findTimezoneSearchBox().vm.$emit('input', searchTerm);
await wrapper.vm.$nextTick();
const options = findDropdownOptions();
expect(options.length).toBe(1);
expect(options.at(0).text()).toContain(searchTerm);
});
it('should display no results item when there are no filter matches', async () => {
const searchTerm = 'someUnexistentTZ';
findTimezoneSearchBox().vm.$emit('input', searchTerm);
await wrapper.vm.$nextTick();
const options = findDropdownOptions();
expect(options.length).toBe(1);
expect(options.at(0).text()).toContain(i18n.noResults);
});
});
it('should add a checkmark to the selected option', async () => {
const selectedTZOption = findDropdownOptions().at(0);
selectedTZOption.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(selectedTZOption.attributes('ischecked')).toBe('true');
});
});
describe('Schedule create', () => { describe('Schedule create', () => {
it('makes a request with form data to create a schedule', () => { it('makes a request with form data to create a schedule', () => {
mutate.mockResolvedValueOnce({}); mutate.mockResolvedValueOnce({});
...@@ -121,20 +82,4 @@ describe('Add schedule modal', () => { ...@@ -121,20 +82,4 @@ describe('Add schedule modal', () => {
expect(alert.text()).toContain(error); expect(alert.text()).toContain(error);
}); });
}); });
describe('Form validation', () => {
describe('Timezone select', () => {
it('has red border when nothing selected', () => {
expect(findTimezoneDropdown().classes()).toContain('invalid-dropdown');
});
it("doesn't have a red border when there is selected opeion", async () => {
findDropdownOptions()
.at(1)
.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(findTimezoneDropdown().classes()).not.toContain('invalid-dropdown');
});
});
});
}); });
/* eslint-disable no-unused-vars */
import { shallowMount, createLocalVue } from '@vue/test-utils';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import { GlModal, GlAlert, GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import destroyOncallScheduleMutation from 'ee/oncall_schedules/graphql/mutations/destroy_oncall_schedule.mutation.graphql';
import DeleteScheduleModal, {
i18n,
} from 'ee/oncall_schedules/components/delete_schedule_modal.vue';
import { getOncallSchedulesQueryResponse, destroyScheduleResponse } from './mocks/apollo_mock';
const localVue = createLocalVue();
const projectPath = 'group/project';
const mutate = jest.fn();
const mockHideModal = jest.fn();
localVue.use(VueApollo);
describe('DeleteScheduleModal', () => {
let wrapper;
let fakeApollo;
let destroyScheduleHandler;
const findModal = () => wrapper.find(GlModal);
const findModalText = () => wrapper.find(GlSprintf);
const findAlert = () => wrapper.find(GlAlert);
async function awaitApolloDomMock() {
await wrapper.vm.$nextTick(); // kick off the DOM update
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
await wrapper.vm.$nextTick(); // kick off the DOM update for flash
}
async function destroySchedule(localWrapper) {
await jest.runOnlyPendingTimers();
await localWrapper.vm.$nextTick();
localWrapper.vm.$emit('primary');
}
const createComponent = ({ data = {}, props = {} } = {}) => {
wrapper = shallowMount(DeleteScheduleModal, {
data() {
return {
...data,
};
},
propsData: {
schedule:
getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0],
...props,
},
provide: {
projectPath,
},
mocks: {
$apollo: {
mutate,
},
},
stubs: { GlSprintf: false },
});
wrapper.vm.$refs.deleteScheduleModal.hide = mockHideModal;
};
function createComponentWithApollo({
destroyHandler = jest.fn().mockResolvedValue(destroyScheduleResponse),
} = {}) {
localVue.use(VueApollo);
destroyScheduleHandler = destroyHandler;
const requestHandlers = [[destroyOncallScheduleMutation, destroyScheduleHandler]];
fakeApollo = createMockApollo(requestHandlers);
wrapper = shallowMount(DeleteScheduleModal, {
localVue,
apolloProvider: fakeApollo,
provide: {
projectPath,
},
});
}
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders delete schedule modal layout', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('renders delete modal with the correct schedule information', () => {
it('renders name of schedule to destroy', () => {
expect(findModalText().attributes('message')).toBe(i18n.deleteScheduleMessage);
});
});
describe('Schedule destroy apollo API call', () => {
it('makes a request with `oncallScheduleDestroy` to delete a schedule', () => {
mutate.mockResolvedValueOnce({});
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
expect(mutate).toHaveBeenCalledWith({
mutation: expect.any(Object),
update: expect.anything(),
// TODO: Once the BE is complete for the mutation update this spec to use the correct params
variables: expect.anything(),
});
});
it('hides the modal on successful schedule deletion', async () => {
mutate.mockResolvedValueOnce({ data: { oncallScheduleDestroy: { errors: [] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
expect(mockHideModal).toHaveBeenCalled();
});
it("doesn't hide the modal on deletion fail", async () => {
const error = 'some error';
mutate.mockResolvedValueOnce({ data: { oncallScheduleDestroy: { errors: [error] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
const alert = findAlert();
expect(mockHideModal).not.toHaveBeenCalled();
expect(alert.exists()).toBe(true);
expect(alert.text()).toContain(error);
});
});
describe('with mocked Apollo client', () => {
// TODO: Once the BE is complete for the mutation add specs here for that via a destroyHandler
});
});
/* eslint-disable no-unused-vars */
import { shallowMount, createLocalVue } from '@vue/test-utils';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import { GlModal } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import updateOncallScheduleMutation from 'ee/oncall_schedules/graphql/mutations/update_oncall_schedule.mutation.graphql';
import UpdateScheduleModal, { i18n } from 'ee/oncall_schedules/components/edit_schedule_modal.vue';
import { UPDATE_SCHEDULE_ERROR } from 'ee/oncall_schedules/utils/error_messages';
import { getOncallSchedulesQueryResponse, updateScheduleResponse } from './mocks/apollo_mock';
import mockTimezones from './mocks/mockTimezones.json';
const localVue = createLocalVue();
const projectPath = 'group/project';
const mutate = jest.fn();
const mockHideModal = jest.fn();
localVue.use(VueApollo);
describe('UpdateScheduleModal', () => {
let wrapper;
let fakeApollo;
let updateScheduleHandler;
const findModal = () => wrapper.find(GlModal);
async function awaitApolloDomMock() {
await wrapper.vm.$nextTick(); // kick off the DOM update
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
await wrapper.vm.$nextTick(); // kick off the DOM update for flash
}
async function destroySchedule(localWrapper) {
await jest.runOnlyPendingTimers();
await localWrapper.vm.$nextTick();
localWrapper.vm.$emit('primary');
}
const createComponent = ({ data = {}, props = {} } = {}) => {
wrapper = shallowMount(UpdateScheduleModal, {
data() {
return {
...data,
form:
getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0],
};
},
propsData: {
schedule:
getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0],
...props,
},
provide: {
projectPath,
timezones: mockTimezones,
},
mocks: {
$apollo: {
mutate,
},
},
});
wrapper.vm.$refs.updateScheduleModal.hide = mockHideModal;
};
function createComponentWithApollo({
updateHandler = jest.fn().mockResolvedValue(updateScheduleResponse),
} = {}) {
localVue.use(VueApollo);
updateScheduleHandler = updateHandler;
const requestHandlers = [[updateOncallScheduleMutation, updateScheduleHandler]];
fakeApollo = createMockApollo(requestHandlers);
wrapper = shallowMount(UpdateScheduleModal, {
localVue,
apolloProvider: fakeApollo,
provide: {
projectPath,
timezones: mockTimezones,
},
});
}
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders update schedule modal layout', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('renders update modal with the correct schedule information', () => {
it('renders name of correct modal id', () => {
expect(findModal().attributes('modalid')).toBe('updateScheduleModal');
});
it('renders name of schedule to update', () => {
expect(findModal().html()).toContain(i18n.editSchedule);
});
});
describe('Schedule update apollo API call', () => {
it('makes a request with `oncallScheduleUpdate` to update a schedule', () => {
mutate.mockResolvedValueOnce({});
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
expect(mutate).toHaveBeenCalledWith({
mutation: expect.any(Object),
update: expect.anything(),
// TODO: Once the BE is complete for the mutation update this spec to use the correct params
variables: expect.anything(),
});
});
it('hides the modal on successful schedule creation', async () => {
mutate.mockResolvedValueOnce({ data: { oncallScheduleUpdate: { errors: [] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
// TODO: Once the BE is complete for the mutation update this spec to use the call
expect(mockHideModal).not.toHaveBeenCalled();
});
it("doesn't hide the modal on fail", async () => {
const error = 'some error';
mutate.mockResolvedValueOnce({ data: { oncallScheduleUpdate: { errors: [error] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
expect(mockHideModal).not.toHaveBeenCalled();
});
});
describe('with mocked Apollo client', () => {
// TODO: Once the BE is complete for the mutation add specs here for that via a destroyHandler
});
});
...@@ -14,3 +14,70 @@ export const participants = [ ...@@ -14,3 +14,70 @@ export const participants = [
avatarUrl: '', avatarUrl: '',
}, },
]; ];
export const errorMsg = 'Something went wrong';
export const getOncallSchedulesQueryResponse = {
data: {
project: {
incidentManagementOncallSchedules: {
nodes: [
{
iid: '37',
name: 'Test schedule',
description: 'Description 1 lives here',
timezone: 'Pacific/Honolulu',
},
],
},
},
},
};
export const scheduleToDestroy = {
iid: '37',
name: 'Test schedule',
description: 'Description 1 lives here',
timezone: 'Pacific/Honolulu',
};
export const destroyScheduleResponse = {
data: {
oncallScheduleDestroy: {
errors: [],
oncallSchedule: {
iid: '37',
name: 'Test schedule',
description: 'Description 1 lives here',
timezone: 'Pacific/Honolulu',
},
},
},
};
export const destroyScheduleResponseWithErrors = {
data: {
oncallScheduleDestroy: {
errors: ['Houston, we have a problem'],
oncallSchedule: {
iid: '37',
name: 'Test schedule',
description: 'Description 1 lives here',
timezone: 'Pacific/Honolulu',
},
},
},
};
export const updateScheduleResponse = {
data: {
oncallScheduleDestroy: {
errors: [],
oncallSchedule: {
iid: '37',
name: 'Test schedule 2',
description: 'Description 2 lives here',
timezone: 'Pacific/Honolulu',
},
},
},
};
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import ScheduleTimelineSection from 'ee/oncall_schedules/components/schedule/components/schedule_timeline_section.vue'; import { GlCard, GlButton } from '@gitlab/ui';
import ScheduleTimelineSection, {
i18n,
} 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/components/schedule/constants'; import { PRESET_TYPES } from 'ee/oncall_schedules/components/schedule/constants';
...@@ -9,6 +12,9 @@ describe('RoadmapTimelineSectionComponent', () => { ...@@ -9,6 +12,9 @@ describe('RoadmapTimelineSectionComponent', () => {
const mockTimeframeInitialDate = new Date(2018, 0, 1); const mockTimeframeInitialDate = new Date(2018, 0, 1);
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate); const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
const findRotations = () => wrapper.find(GlCard);
const findAddRotation = () => wrapper.find(GlButton);
function mountComponent({ function mountComponent({
presetType = PRESET_TYPES.WEEKS, presetType = PRESET_TYPES.WEEKS,
timeframe = mockTimeframeWeeks, timeframe = mockTimeframeWeeks,
...@@ -18,6 +24,9 @@ describe('RoadmapTimelineSectionComponent', () => { ...@@ -18,6 +24,9 @@ describe('RoadmapTimelineSectionComponent', () => {
presetType, presetType,
timeframe, timeframe,
}, },
stubs: {
GlCard,
},
}); });
} }
...@@ -33,7 +42,7 @@ describe('RoadmapTimelineSectionComponent', () => { ...@@ -33,7 +42,7 @@ describe('RoadmapTimelineSectionComponent', () => {
}); });
it('renders component container element with class `timeline-section`', () => { it('renders component container element with class `timeline-section`', () => {
expect(wrapper.classes()).toContain('timeline-section'); expect(wrapper.html()).toContain('timeline-section');
}); });
it('renders empty header cell element with class `timeline-header-blank`', () => { it('renders empty header cell element with class `timeline-header-blank`', () => {
...@@ -43,4 +52,13 @@ describe('RoadmapTimelineSectionComponent', () => { ...@@ -43,4 +52,13 @@ describe('RoadmapTimelineSectionComponent', () => {
it('renders weeks header items based on timeframe data', () => { it('renders weeks header items based on timeframe data', () => {
expect(wrapper.findAll(WeeksHeaderItem).length).toBe(mockTimeframeWeeks.length); expect(wrapper.findAll(WeeksHeaderItem).length).toBe(mockTimeframeWeeks.length);
}); });
it('renders the rotation card wrapper', () => {
expect(findRotations().exists()).toBe(true);
});
it('renders the add rotation button in the rotation card wrapper', () => {
expect(findAddRotation().exists()).toBe(true);
expect(findAddRotation().text()).toBe(i18n.addARotation);
});
}); });
...@@ -19097,6 +19097,9 @@ msgstr "" ...@@ -19097,6 +19097,9 @@ msgstr ""
msgid "On-call schedules" msgid "On-call schedules"
msgstr "" msgstr ""
msgid "OnCallSchedules|Add a rotation"
msgstr ""
msgid "OnCallSchedules|Add a schedule" msgid "OnCallSchedules|Add a schedule"
msgstr "" msgstr ""
...@@ -19106,15 +19109,27 @@ msgstr "" ...@@ -19106,15 +19109,27 @@ msgstr ""
msgid "OnCallSchedules|Add schedule" msgid "OnCallSchedules|Add schedule"
msgstr "" msgstr ""
msgid "OnCallSchedules|Are you sure you want to delete the \"%{deleteSchedule}\" schedule? This action cannot be undone."
msgstr ""
msgid "OnCallSchedules|Create on-call schedules in GitLab" msgid "OnCallSchedules|Create on-call schedules in GitLab"
msgstr "" msgstr ""
msgid "OnCallSchedules|Delete schedule"
msgstr ""
msgid "OnCallSchedules|Edit schedule"
msgstr ""
msgid "OnCallSchedules|Failed to add rotation" msgid "OnCallSchedules|Failed to add rotation"
msgstr "" msgstr ""
msgid "OnCallSchedules|Failed to add schedule" msgid "OnCallSchedules|Failed to add schedule"
msgstr "" msgstr ""
msgid "OnCallSchedules|Failed to edit schedule"
msgstr ""
msgid "OnCallSchedules|On-call schedule" msgid "OnCallSchedules|On-call schedule"
msgstr "" msgstr ""
...@@ -19133,6 +19148,9 @@ msgstr "" ...@@ -19133,6 +19148,9 @@ msgstr ""
msgid "OnCallSchedules|Rotation start date cannot be empty" msgid "OnCallSchedules|Rotation start date cannot be empty"
msgstr "" msgstr ""
msgid "OnCallSchedules|Rotations"
msgstr ""
msgid "OnCallSchedules|Route alerts directly to specific members of your team" msgid "OnCallSchedules|Route alerts directly to specific members of your team"
msgstr "" msgstr ""
...@@ -19145,6 +19163,12 @@ msgstr "" ...@@ -19145,6 +19163,12 @@ 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|The schedule could not be deleted. Please try again."
msgstr ""
msgid "OnCallSchedules|The schedule could not be updated. Please try again."
msgstr ""
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later." msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment