Commit 082f68e2 authored by Simon Knox's avatar Simon Knox

Merge branch '262852-adjsut-schedule-timeframe-loading' into 'master'

Fix(oncallschedule): add base loader

See merge request gitlab-org/gitlab!52917
parents 8575766b 9be7d887
...@@ -9,7 +9,6 @@ import { ...@@ -9,7 +9,6 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { capitalize } from 'lodash'; import { capitalize } from 'lodash';
import { fetchPolicies } from '~/lib/graphql';
import { import {
formatDate, formatDate,
nWeeksBefore, nWeeksBefore,
...@@ -68,7 +67,6 @@ export default { ...@@ -68,7 +67,6 @@ export default {
}, },
apollo: { apollo: {
rotations: { rotations: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
query: getShiftsForRotations, query: getShiftsForRotations,
variables() { variables() {
const startsAt = this.timeframeStartDate; const startsAt = this.timeframeStartDate;
...@@ -123,7 +121,7 @@ export default { ...@@ -123,7 +121,7 @@ export default {
return ''; return '';
} }
}, },
isLoading() { loading() {
return this.$apollo.queries.rotations.loading; return this.$apollo.queries.rotations.loading;
}, },
}, },
...@@ -159,6 +157,9 @@ export default { ...@@ -159,6 +157,9 @@ export default {
break; break;
} }
}, },
fetchRotationShifts() {
this.$apollo.queries.rotations.refetch();
},
}, },
}; };
</script> </script>
...@@ -215,13 +216,13 @@ export default { ...@@ -215,13 +216,13 @@ export default {
<gl-button <gl-button
data-testid="previous-timeframe-btn" data-testid="previous-timeframe-btn"
icon="chevron-left" icon="chevron-left"
:disabled="isLoading" :disabled="loading"
@click="updateToViewPreviousTimeframe" @click="updateToViewPreviousTimeframe"
/> />
<gl-button <gl-button
data-testid="next-timeframe-btn" data-testid="next-timeframe-btn"
icon="chevron-right" icon="chevron-right"
:disabled="isLoading" :disabled="loading"
@click="updateToViewNextTimeframe" @click="updateToViewNextTimeframe"
/> />
</gl-button-group> </gl-button-group>
...@@ -248,6 +249,7 @@ export default { ...@@ -248,6 +249,7 @@ export default {
:rotations="rotations" :rotations="rotations"
:timeframe="timeframe" :timeframe="timeframe"
:schedule-iid="schedule.iid" :schedule-iid="schedule.iid"
:loading="loading"
/> />
</div> </div>
</gl-card> </gl-card>
...@@ -258,7 +260,11 @@ export default { ...@@ -258,7 +260,11 @@ export default {
:modal-id="$options.editScheduleModalId" :modal-id="$options.editScheduleModalId"
is-edit-mode is-edit-mode
/> />
<add-edit-rotation-modal :schedule="schedule" :modal-id="$options.addRotationModalId" /> <add-edit-rotation-modal
:schedule="schedule"
:modal-id="$options.addRotationModalId"
@fetchRotationShifts="fetchRotationShifts"
/>
<add-edit-rotation-modal <add-edit-rotation-modal
:schedule="schedule" :schedule="schedule"
:modal-id="$options.editRotationModalId" :modal-id="$options.editRotationModalId"
......
...@@ -5,10 +5,7 @@ import { LENGTH_ENUM } from 'ee/oncall_schedules/constants'; ...@@ -5,10 +5,7 @@ import { LENGTH_ENUM } from 'ee/oncall_schedules/constants';
import createOncallScheduleRotationMutation from 'ee/oncall_schedules/graphql/mutations/create_oncall_schedule_rotation.mutation.graphql'; import createOncallScheduleRotationMutation from 'ee/oncall_schedules/graphql/mutations/create_oncall_schedule_rotation.mutation.graphql';
import updateOncallScheduleRotationMutation from 'ee/oncall_schedules/graphql/mutations/update_oncall_schedule_rotation.mutation.graphql'; import updateOncallScheduleRotationMutation from 'ee/oncall_schedules/graphql/mutations/update_oncall_schedule_rotation.mutation.graphql';
import getOncallSchedulesWithRotationsQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql'; import getOncallSchedulesWithRotationsQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import { import { updateStoreAfterRotationEdit } from 'ee/oncall_schedules/utils/cache_updates';
updateStoreAfterRotationAdd,
updateStoreAfterRotationEdit,
} from 'ee/oncall_schedules/utils/cache_updates';
import { isNameFieldValid, getParticipantsForSave } from 'ee/oncall_schedules/utils/common_utils'; import { isNameFieldValid, getParticipantsForSave } from 'ee/oncall_schedules/utils/common_utils';
import createFlash, { FLASH_TYPES } from '~/flash'; import createFlash, { FLASH_TYPES } from '~/flash';
import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql'; import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
...@@ -149,22 +146,11 @@ export default { ...@@ -149,22 +146,11 @@ export default {
methods: { methods: {
createRotation() { createRotation() {
this.loading = true; this.loading = true;
const { projectPath, schedule } = this;
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: createOncallScheduleRotationMutation, mutation: createOncallScheduleRotationMutation,
variables: { input: this.rotationVariables }, variables: { input: this.rotationVariables },
update(store, { data }) {
updateStoreAfterRotationAdd(
store,
getOncallSchedulesWithRotationsQuery,
{ ...data, scheduleIid: schedule.iid },
{
projectPath,
},
);
},
}) })
.then( .then(
({ ({
...@@ -179,6 +165,7 @@ export default { ...@@ -179,6 +165,7 @@ export default {
} }
this.$refs.addEditScheduleRotationModal.hide(); this.$refs.addEditScheduleRotationModal.hide();
this.$emit('fetchRotationShifts');
return createFlash({ return createFlash({
message: this.$options.i18n.rotationCreated, message: this.$options.i18n.rotationCreated,
type: FLASH_TYPES.SUCCESS, type: FLASH_TYPES.SUCCESS,
......
<script> <script>
import { GlButtonGroup, GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui'; import {
GlButtonGroup,
GlButton,
GlLoadingIcon,
GlTooltipDirective,
GlModalDirective,
} from '@gitlab/ui';
import DeleteRotationModal from 'ee/oncall_schedules/components/rotations/components/delete_rotation_modal.vue'; import DeleteRotationModal from 'ee/oncall_schedules/components/rotations/components/delete_rotation_modal.vue';
import ScheduleShiftWrapper from 'ee/oncall_schedules/components/schedule/components/shifts/components/schedule_shift_wrapper.vue'; import ScheduleShiftWrapper from 'ee/oncall_schedules/components/schedule/components/shifts/components/schedule_shift_wrapper.vue';
import { import {
...@@ -14,6 +20,7 @@ import CurrentDayIndicator from './current_day_indicator.vue'; ...@@ -14,6 +20,7 @@ import CurrentDayIndicator from './current_day_indicator.vue';
export const i18n = { export const i18n = {
editRotationLabel: s__('OnCallSchedules|Edit rotation'), editRotationLabel: s__('OnCallSchedules|Edit rotation'),
deleteRotationLabel: s__('OnCallSchedules|Delete rotation'), deleteRotationLabel: s__('OnCallSchedules|Delete rotation'),
addRotationLabel: s__('OnCallSchedules|Currently no rotation.'),
}; };
export default { export default {
...@@ -23,6 +30,7 @@ export default { ...@@ -23,6 +30,7 @@ export default {
components: { components: {
GlButton, GlButton,
GlButtonGroup, GlButtonGroup,
GlLoadingIcon,
CurrentDayIndicator, CurrentDayIndicator,
DeleteRotationModal, DeleteRotationModal,
ScheduleShiftWrapper, ScheduleShiftWrapper,
...@@ -48,6 +56,11 @@ export default { ...@@ -48,6 +56,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
loading: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -80,60 +93,78 @@ export default { ...@@ -80,60 +93,78 @@ export default {
cellShouldHideOverflow(index) { cellShouldHideOverflow(index) {
return index + 1 === this.timeframe.length || this.presetIsDay; return index + 1 === this.timeframe.length || this.presetIsDay;
}, },
timeframeItemUniqueKey(timeframeItem) {
return timeframeItem.valueOf();
},
}, },
}; };
</script> </script>
<template> <template>
<div class="list-section"> <div class="list-section">
<div <gl-loading-icon v-if="loading" />
v-for="rotation in rotations" <div v-else-if="rotations.length === 0 && !loading" class="gl-clearfix">
:key="rotation.id"
class="list-item list-item-empty clearfix"
>
<span <span
class="details-cell gl-display-flex gl-justify-content-space-between gl-align-items-center gl-pl-3" class="details-cell gl-display-flex gl-justify-content-space-between gl-align-items-center gl-pl-3"
> >
<span class="gl-str-truncated">{{ rotation.name }}</span> <span class="gl-text-truncated">{{ $options.i18n.addRotationLabel }}</span>
<gl-button-group class="gl-px-2">
<gl-button
v-gl-modal="$options.editRotationModalId"
v-gl-tooltip
category="tertiary"
:title="$options.i18n.editRotationLabel"
icon="pencil"
:aria-label="$options.i18n.editRotationLabel"
:disabled="true"
/>
<gl-button
v-gl-modal="$options.deleteRotationModalId"
v-gl-tooltip
category="tertiary"
:title="$options.i18n.deleteRotationLabel"
icon="remove"
:aria-label="$options.i18n.deleteRotationLabel"
@click="setRotationToUpdate(rotation)"
/>
</gl-button-group>
</span> </span>
<span <span
v-for="(timeframeItem, index) in timeframeToDraw" v-for="(timeframeItem, index) in timeframeToDraw"
:key="index" :key="index"
class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1" class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1"
:class="{ 'gl-overflow-hidden': cellShouldHideOverflow(index) }"
:style="timelineStyles" :style="timelineStyles"
data-testid="timelineCell" data-testid="empty-timeline-cell"
> >
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" /> <current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" />
<schedule-shift-wrapper
v-if="rotation.shifts"
:preset-type="presetType"
:timeframe-item="timeframeItem"
:timeframe="timeframe"
:rotation="rotation"
/>
</span> </span>
</div> </div>
<div v-else>
<div v-for="rotation in rotations" :key="rotation.id" class="gl-clearfix">
<span
class="details-cell gl-display-flex gl-justify-content-space-between gl-align-items-center gl-pl-3"
>
<span class="gl-text-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"
icon="pencil"
:aria-label="$options.i18n.editRotationLabel"
:disabled="true"
/>
<gl-button
v-gl-modal="$options.deleteRotationModalId"
v-gl-tooltip
category="tertiary"
:title="$options.i18n.deleteRotationLabel"
icon="remove"
:aria-label="$options.i18n.deleteRotationLabel"
@click="setRotationToUpdate(rotation)"
/>
</gl-button-group>
</span>
<span
v-for="(timeframeItem, index) in timeframeToDraw"
:key="timeframeItemUniqueKey(timeframeItem)"
class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1"
:class="{ 'gl-overflow-hidden': cellShouldHideOverflow(index) }"
:style="timelineStyles"
data-testid="timeline-cell"
>
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" />
<schedule-shift-wrapper
v-if="rotation.shifts"
:preset-type="presetType"
:timeframe-item="timeframeItem"
:timeframe="timeframe"
:rotation="rotation"
/>
</span>
</div>
</div>
<delete-rotation-modal <delete-rotation-modal
:rotation="rotationToUpdate" :rotation="rotationToUpdate"
:schedule-iid="scheduleIid" :schedule-iid="scheduleIid"
......
<script> <script>
import { PRESET_TYPES } from 'ee/oncall_schedules/constants'; import { PRESET_TYPES, DAYS_IN_DATE_WEEK } from 'ee/oncall_schedules/constants';
import getShiftTimeUnitWidthQuery from 'ee/oncall_schedules/graphql/queries/get_shift_time_unit_width.query.graphql'; import getShiftTimeUnitWidthQuery from 'ee/oncall_schedules/graphql/queries/get_shift_time_unit_width.query.graphql';
import { getOverlapDateInPeriods, nDaysAfter } from '~/lib/utils/datetime_utility';
import DaysScheduleShift from './days_schedule_shift.vue'; import DaysScheduleShift from './days_schedule_shift.vue';
import WeeksScheduleShift from './weeks_schedule_shift.vue'; import WeeksScheduleShift from './weeks_schedule_shift.vue';
...@@ -41,6 +42,31 @@ export default { ...@@ -41,6 +42,31 @@ export default {
query: getShiftTimeUnitWidthQuery, query: getShiftTimeUnitWidthQuery,
}, },
}, },
computed: {
currentTimeframeEndsAt() {
return new Date(
nDaysAfter(
this.timeframeItem,
this.presetType === PRESET_TYPES.DAYS ? 1 : DAYS_IN_DATE_WEEK,
),
);
},
shiftsToRender() {
const validShifts = this.rotation.shifts.nodes.filter(
({ startsAt, endsAt }) => this.shiftRangeOverlap(startsAt, endsAt).hoursOverlap > 0,
);
// TODO: If week view and on same day, dont show more than 1 assignee or use CSS to limit their size to be readable
return Object.freeze(validShifts);
},
},
methods: {
shiftRangeOverlap(shiftStartsAt, shiftEndsAt) {
return getOverlapDateInPeriods(
{ start: this.timeframeItem, end: this.currentTimeframeEndsAt },
{ start: shiftStartsAt, end: shiftEndsAt },
);
},
},
}; };
</script> </script>
...@@ -48,7 +74,7 @@ export default { ...@@ -48,7 +74,7 @@ export default {
<div> <div>
<component <component
:is="componentByPreset[presetType]" :is="componentByPreset[presetType]"
v-for="(shift, shiftIndex) in rotation.shifts.nodes" v-for="(shift, shiftIndex) in shiftsToRender"
:key="shift.startAt" :key="shift.startAt"
:shift="shift" :shift="shift"
:shift-index="shiftIndex" :shift-index="shiftIndex"
......
...@@ -38,6 +38,10 @@ export default { ...@@ -38,6 +38,10 @@ export default {
return nDaysAfter(this.timeframeItem, DAYS_IN_DATE_WEEK); return nDaysAfter(this.timeframeItem, DAYS_IN_DATE_WEEK);
}, },
daysUntilEndOfTimeFrame() { daysUntilEndOfTimeFrame() {
if (this.currentTimeframeEndsAt.getMonth() !== this.timeframeItem.getMonth()) {
// TODO: Handle Edge case where timeframe spans two different months
}
return ( return (
this.currentTimeframeEndsAt.getDate() - this.currentTimeframeEndsAt.getDate() -
new Date(this.shiftRangeOverlap.overlapStartDate).getDate() + new Date(this.shiftRangeOverlap.overlapStartDate).getDate() +
...@@ -73,6 +77,8 @@ export default { ...@@ -73,6 +77,8 @@ export default {
}, },
shiftShouldRender() { shiftShouldRender() {
if (this.timeFrameIndex !== 0) { if (this.timeFrameIndex !== 0) {
// TDOD: Handle edge case where this.shiftRangeOverlap.overlapStartDate is the same as this.timeframeItem
return ( return (
new Date(this.shiftRangeOverlap.overlapStartDate) > this.timeframeItem && new Date(this.shiftRangeOverlap.overlapStartDate) > this.timeframeItem &&
new Date(this.shiftRangeOverlap.overlapStartDate) < this.currentTimeframeEndsAt new Date(this.shiftRangeOverlap.overlapStartDate) < this.currentTimeframeEndsAt
...@@ -96,9 +102,9 @@ export default { ...@@ -96,9 +102,9 @@ export default {
const baseWidth = const baseWidth =
this.timeFrameIndex === 0 this.timeFrameIndex === 0
? this.totalShiftRangeOverlap.daysOverlap ? this.totalShiftRangeOverlap.daysOverlap
: this.shiftRangeOverlap.daysOverlap; : this.shiftRangeOverlap.daysOverlap + offset;
return baseWidth + offset; return baseWidth;
}, },
timeFrameIndex() { timeFrameIndex() {
return this.timeframe.indexOf(this.timeframeItem); return this.timeframe.indexOf(this.timeframeItem);
......
...@@ -87,32 +87,6 @@ const updateScheduleFromStore = (store, query, { oncallScheduleUpdate }, variabl ...@@ -87,32 +87,6 @@ const updateScheduleFromStore = (store, query, { oncallScheduleUpdate }, variabl
}); });
}; };
const addRotationToStore = (store, query, { oncallRotationCreate, scheduleIid }, variables) => {
const rotation = oncallRotationCreate?.oncallRotation;
if (!rotation) {
return;
}
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, (draftData) => {
const scheduleToUpdate = draftData.project.incidentManagementOncallSchedules.nodes.find(
({ iid }) => iid === scheduleIid,
);
scheduleToUpdate.rotations.nodes = [...scheduleToUpdate.rotations.nodes, rotation];
});
store.writeQuery({
query,
variables,
data,
});
};
const updateRotationFromStore = ( const updateRotationFromStore = (
store, store,
query, query,
...@@ -212,14 +186,6 @@ export const updateStoreAfterScheduleEdit = (store, query, data, variables) => { ...@@ -212,14 +186,6 @@ export const updateStoreAfterScheduleEdit = (store, query, data, variables) => {
} }
}; };
export const updateStoreAfterRotationAdd = (store, query, data, variables) => {
if (hasErrors(data)) {
onError(data, UPDATE_SCHEDULE_ERROR);
} else {
addRotationToStore(store, query, data, variables);
}
};
export const updateStoreAfterRotationEdit = (store, query, data, variables) => { export const updateStoreAfterRotationEdit = (store, query, data, variables) => {
if (hasErrors(data)) { if (hasErrors(data)) {
onError(data, UPDATE_ROTATION_ERROR); onError(data, UPDATE_ROTATION_ERROR);
......
...@@ -66,7 +66,7 @@ describe('On-call schedule', () => { ...@@ -66,7 +66,7 @@ describe('On-call schedule', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(utils, 'getTimeframeForWeeksView').mockReturnValue(mockWeeksTimeFrame); jest.spyOn(utils, 'getTimeframeForWeeksView').mockReturnValue(mockWeeksTimeFrame);
jest.spyOn(commonUtils, 'getFormattedTimezone').mockReturnValue(formattedTimezone); jest.spyOn(commonUtils, 'getFormattedTimezone').mockReturnValue(formattedTimezone);
createComponent({ schedule: mockSchedule }); createComponent({ schedule: mockSchedule, loading: false });
}); });
afterEach(() => { afterEach(() => {
...@@ -119,6 +119,7 @@ describe('On-call schedule', () => { ...@@ -119,6 +119,7 @@ describe('On-call schedule', () => {
timeframe: mockWeeksTimeFrame, timeframe: mockWeeksTimeFrame,
rotations: expect.any(Array), rotations: expect.any(Array),
scheduleIid: mockSchedule.iid, scheduleIid: mockSchedule.iid,
loading: wrapper.vm.$apollo.queries.rotations.loading,
}); });
}); });
......
...@@ -142,7 +142,6 @@ describe('AddEditRotationModal', () => { ...@@ -142,7 +142,6 @@ describe('AddEditRotationModal', () => {
findModal().vm.$emit('primary', { preventDefault: jest.fn() }); findModal().vm.$emit('primary', { preventDefault: jest.fn() });
expect(mutate).toHaveBeenCalledWith({ expect(mutate).toHaveBeenCalledWith({
mutation: expect.any(Object), mutation: expect.any(Object),
update: expect.anything(),
variables: { input: expect.objectContaining({ projectPath }) }, variables: { input: expect.objectContaining({ projectPath }) },
}); });
}); });
...@@ -174,6 +173,7 @@ describe('AddEditRotationModal', () => { ...@@ -174,6 +173,7 @@ describe('AddEditRotationModal', () => {
it('calls a mutation with correct parameters and creates a rotation', async () => { it('calls a mutation with correct parameters and creates a rotation', async () => {
createComponentWithApollo(); createComponentWithApollo();
expect(wrapper.emitted('fetchRotationShifts')).toBeUndefined();
await createRotation(wrapper); await createRotation(wrapper);
await awaitApolloDomMock(); await awaitApolloDomMock();
...@@ -184,6 +184,7 @@ describe('AddEditRotationModal', () => { ...@@ -184,6 +184,7 @@ describe('AddEditRotationModal', () => {
message: i18n.rotationCreated, message: i18n.rotationCreated,
type: FLASH_TYPES.SUCCESS, type: FLASH_TYPES.SUCCESS,
}); });
expect(wrapper.emitted('fetchRotationShifts')).toHaveLength(1);
}); });
it('displays alert if mutation had a recoverable error', async () => { it('displays alert if mutation had a recoverable error', async () => {
......
...@@ -35,7 +35,7 @@ describe('RotationsListSectionComponent', () => { ...@@ -35,7 +35,7 @@ describe('RotationsListSectionComponent', () => {
}); });
} }
const findTimelineCells = () => wrapper.findAll('[data-testid="timelineCell"]'); const findTimelineCells = () => wrapper.findAll('[data-testid="timeline-cell"]');
const findRotationAssignees = () => wrapper.findAllComponents(RotationsAssignee); const findRotationAssignees = () => wrapper.findAllComponents(RotationsAssignee);
const findCurrentDayIndicatorContent = () => const findCurrentDayIndicatorContent = () =>
wrapper.find('[data-testid="current-day-indicator"]'); wrapper.find('[data-testid="current-day-indicator"]');
......
...@@ -56,7 +56,7 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/s ...@@ -56,7 +56,7 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/s
describe('when the preset type is DAYS', () => { describe('when the preset type is DAYS', () => {
it('should render a selection of day grid shifts inside the rotation', () => { it('should render a selection of day grid shifts inside the rotation', () => {
createComponent({ props: { presetType: PRESET_TYPES.DAYS } }); createComponent({ props: { presetType: PRESET_TYPES.DAYS } });
expect(findDaysScheduleShifts()).toHaveLength(2); expect(findDaysScheduleShifts()).toHaveLength(1);
}); });
}); });
}); });
...@@ -75,7 +75,7 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/w ...@@ -75,7 +75,7 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/w
}); });
expect(findRotationAssignee().props('rotationAssigneeStyle')).toEqual({ expect(findRotationAssignee().props('rotationAssigneeStyle')).toEqual({
left: '52px', left: '52px',
width: '100px', width: '50px',
}); });
}); });
}); });
......
...@@ -20655,6 +20655,9 @@ msgstr "" ...@@ -20655,6 +20655,9 @@ msgstr ""
msgid "OnCallSchedules|Create on-call schedules in GitLab" msgid "OnCallSchedules|Create on-call schedules in GitLab"
msgstr "" msgstr ""
msgid "OnCallSchedules|Currently no rotation."
msgstr ""
msgid "OnCallSchedules|Delete rotation" msgid "OnCallSchedules|Delete rotation"
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