Commit f88e9e0b authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '346171-frontend-for-metric-image-url-text' into 'master'

Allow urlText to be saved on metric images

See merge request gitlab-org/gitlab!79811
parents c832a374 2c50c5d8
...@@ -328,7 +328,7 @@ export default { ...@@ -328,7 +328,7 @@ export default {
return axios.get(metricImagesUrl); return axios.get(metricImagesUrl);
}, },
uploadIssueMetricImage({ issueIid, id, file, url = null }) { uploadIssueMetricImage({ issueIid, id, file, url = null, urlText = null }) {
const options = { headers: { ...ContentTypeMultipartFormData } }; const options = { headers: { ...ContentTypeMultipartFormData } };
const metricImagesUrl = Api.buildUrl(this.issueMetricImagesPath) const metricImagesUrl = Api.buildUrl(this.issueMetricImagesPath)
.replace(':id', encodeURIComponent(id)) .replace(':id', encodeURIComponent(id))
...@@ -340,10 +340,31 @@ export default { ...@@ -340,10 +340,31 @@ export default {
if (url) { if (url) {
formData.append('url', url); formData.append('url', url);
} }
if (urlText) {
formData.append('url_text', urlText);
}
return axios.post(metricImagesUrl, formData, options); return axios.post(metricImagesUrl, formData, options);
}, },
updateIssueMetricImage({ issueIid, id, imageId, url = null, urlText = null }) {
const metricImagesUrl = Api.buildUrl(this.issueMetricSingleImagePath)
.replace(':id', encodeURIComponent(id))
.replace(':issue_iid', encodeURIComponent(issueIid))
.replace(':image_id', encodeURIComponent(imageId));
// Construct multipart form data
const formData = new FormData();
if (url != null) {
formData.append('url', url);
}
if (urlText != null) {
formData.append('url_text', urlText);
}
return axios.put(metricImagesUrl, formData);
},
deleteMetricImage({ issueIid, id, imageId }) { deleteMetricImage({ issueIid, id, imageId }) {
const individualMetricImageUrl = Api.buildUrl(this.issueMetricSingleImagePath) const individualMetricImageUrl = Api.buildUrl(this.issueMetricSingleImagePath)
.replace(':id', encodeURIComponent(id)) .replace(':id', encodeURIComponent(id))
......
<script> <script>
import { GlButton, GlCard, GlIcon, GlLink, GlModal, GlSprintf } from '@gitlab/ui'; import {
GlButton,
GlFormGroup,
GlFormInput,
GlCard,
GlIcon,
GlLink,
GlModal,
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
...@@ -9,15 +19,24 @@ export default { ...@@ -9,15 +19,24 @@ export default {
modalDescription: s__('Incident|Are you sure you wish to delete this image?'), modalDescription: s__('Incident|Are you sure you wish to delete this image?'),
modalCancel: __('Cancel'), modalCancel: __('Cancel'),
modalTitle: s__('Incident|Deleting %{filename}'), modalTitle: s__('Incident|Deleting %{filename}'),
editModalUpdate: __('Update'),
editModalTitle: s__('Incident|Editing %{filename}'),
editIconTitle: s__('Incident|Edit image text or link'),
deleteIconTitle: s__('Incident|Delete image'),
}, },
components: { components: {
GlButton, GlButton,
GlFormGroup,
GlFormInput,
GlCard, GlCard,
GlIcon, GlIcon,
GlLink, GlLink,
GlModal, GlModal,
GlSprintf, GlSprintf,
}, },
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['canUpdate'], inject: ['canUpdate'],
props: { props: {
id: { id: {
...@@ -37,16 +56,25 @@ export default { ...@@ -37,16 +56,25 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
urlText: {
type: String,
required: false,
default: null,
},
}, },
data() { data() {
return { return {
isCollapsed: false, isCollapsed: false,
isDeleting: false, isDeleting: false,
isUpdating: false,
modalVisible: false, modalVisible: false,
editModalVisible: false,
modalUrl: this.url,
modalUrlText: this.urlText,
}; };
}, },
computed: { computed: {
actionPrimaryProps() { deleteActionPrimaryProps() {
return { return {
text: this.$options.i18n.modalDelete, text: this.$options.i18n.modalDelete,
attributes: { attributes: {
...@@ -57,6 +85,17 @@ export default { ...@@ -57,6 +85,17 @@ export default {
}, },
}; };
}, },
updateActionPrimaryProps() {
return {
text: this.$options.i18n.editModalUpdate,
attributes: {
loading: this.isUpdating,
disabled: this.isUpdating,
category: 'primary',
variant: 'confirm',
},
};
},
arrowIconName() { arrowIconName() {
return this.isCollapsed ? 'chevron-right' : 'chevron-down'; return this.isCollapsed ? 'chevron-right' : 'chevron-down';
}, },
...@@ -70,10 +109,16 @@ export default { ...@@ -70,10 +109,16 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['deleteImage']), ...mapActions(['deleteImage', 'updateImage']),
toggleCollapsed() { toggleCollapsed() {
this.isCollapsed = !this.isCollapsed; this.isCollapsed = !this.isCollapsed;
}, },
resetEditFields() {
this.modalUrl = this.url;
this.modalUrlText = this.urlText;
this.editModalVisible = false;
this.modalVisible = false;
},
async onDelete() { async onDelete() {
try { try {
this.isDeleting = true; this.isDeleting = true;
...@@ -83,6 +128,21 @@ export default { ...@@ -83,6 +128,21 @@ export default {
this.modalVisible = false; this.modalVisible = false;
} }
}, },
async onUpdate() {
try {
this.isUpdating = true;
await this.updateImage({
imageId: this.id,
url: this.modalUrl,
urlText: this.modalUrlText,
});
} finally {
this.isUpdating = false;
this.modalUrl = '';
this.modalUrlText = '';
this.editModalVisible = false;
}
},
}, },
}; };
</script> </script>
...@@ -98,10 +158,10 @@ export default { ...@@ -98,10 +158,10 @@ export default {
modal-id="delete-metric-modal" modal-id="delete-metric-modal"
size="sm" size="sm"
:visible="modalVisible" :visible="modalVisible"
:action-primary="actionPrimaryProps" :action-primary="deleteActionPrimaryProps"
:action-cancel="{ text: $options.i18n.modalCancel }" :action-cancel="{ text: $options.i18n.modalCancel }"
@primary.prevent="onDelete" @primary.prevent="onDelete"
@hidden="modalVisible = false" @hidden="resetEditFields"
> >
<template #modal-title> <template #modal-title>
<gl-sprintf :message="$options.i18n.modalTitle"> <gl-sprintf :message="$options.i18n.modalTitle">
...@@ -112,6 +172,46 @@ export default { ...@@ -112,6 +172,46 @@ export default {
</template> </template>
<p>{{ $options.i18n.modalDescription }}</p> <p>{{ $options.i18n.modalDescription }}</p>
</gl-modal> </gl-modal>
<gl-modal
modal-id="edit-metric-modal"
size="sm"
:action-primary="updateActionPrimaryProps"
:action-cancel="{ text: $options.i18n.modalCancel }"
:visible="editModalVisible"
data-testid="metric-image-edit-modal"
@hidden="resetEditFields"
@primary.prevent="onUpdate"
>
<template #modal-title>
<gl-sprintf :message="$options.i18n.editModalTitle">
<template #filename>
{{ filename }}
</template>
</gl-sprintf>
</template>
<gl-form-group :label="__('Text (optional)')" label-for="upload-text-input">
<gl-form-input
id="upload-text-input"
v-model="modalUrlText"
data-testid="metric-image-text-field"
/>
</gl-form-group>
<gl-form-group
:label="__('Link (optional)')"
label-for="upload-url-input"
:description="s__('Incidents|Must start with http or https')"
>
<gl-form-input
id="upload-url-input"
v-model="modalUrl"
data-testid="metric-image-url-field"
/>
</gl-form-group>
</gl-modal>
<template #header> <template #header>
<div class="gl-w-full gl-display-flex gl-flex-direction-row gl-justify-content-space-between"> <div class="gl-w-full gl-display-flex gl-flex-direction-row gl-justify-content-space-between">
<div class="gl-display-flex gl-flex-direction-row gl-align-items-center gl-w-full"> <div class="gl-display-flex gl-flex-direction-row gl-align-items-center gl-w-full">
...@@ -125,20 +225,35 @@ export default { ...@@ -125,20 +225,35 @@ export default {
> >
<gl-icon class="gl-mr-2" :name="arrowIconName" /> <gl-icon class="gl-mr-2" :name="arrowIconName" />
</gl-button> </gl-button>
<gl-link v-if="url" :href="url"> <gl-link v-if="url" :href="url" target="_blank" data-testid="metric-image-label-span">
{{ filename }} {{ urlText == null || urlText == '' ? filename : urlText }}
<gl-icon name="external-link" class="gl-vertical-align-middle" />
</gl-link> </gl-link>
<span v-else>{{ filename }}</span> <span v-else data-testid="metric-image-label-span">{{
urlText == null || urlText == '' ? filename : urlText
}}</span>
<div class="gl-ml-auto btn-group">
<gl-button <gl-button
v-if="canUpdate" v-if="canUpdate"
class="gl-ml-auto" v-gl-tooltip.bottom
icon="pencil"
:aria-label="__('Edit')"
:title="$options.i18n.editIconTitle"
data-testid="edit-button"
@click="editModalVisible = true"
/>
<gl-button
v-if="canUpdate"
v-gl-tooltip.bottom
icon="remove" icon="remove"
:aria-label="__('Delete')" :aria-label="__('Delete')"
:title="$options.i18n.deleteIconTitle"
data-testid="delete-button" data-testid="delete-button"
@click="modalVisible = true" @click="modalVisible = true"
/> />
</div> </div>
</div> </div>
</div>
</template> </template>
<div <div
v-show="!isCollapsed" v-show="!isCollapsed"
......
...@@ -22,6 +22,7 @@ export default { ...@@ -22,6 +22,7 @@ export default {
currentFiles: [], currentFiles: [],
modalVisible: false, modalVisible: false,
modalUrl: '', modalUrl: '',
modalUrlText: '',
}; };
}, },
store: createStore(), store: createStore(),
...@@ -34,7 +35,7 @@ export default { ...@@ -34,7 +35,7 @@ export default {
loading: this.isUploadingImage, loading: this.isUploadingImage,
disabled: this.isUploadingImage, disabled: this.isUploadingImage,
category: 'primary', category: 'primary',
variant: 'success', variant: 'confirm',
}, },
}; };
}, },
...@@ -48,6 +49,7 @@ export default { ...@@ -48,6 +49,7 @@ export default {
clearInputs() { clearInputs() {
this.modalVisible = false; this.modalVisible = false;
this.modalUrl = ''; this.modalUrl = '';
this.modalUrlText = '';
this.currentFile = false; this.currentFile = false;
}, },
openMetricDialog(files) { openMetricDialog(files) {
...@@ -56,7 +58,11 @@ export default { ...@@ -56,7 +58,11 @@ export default {
}, },
async onUpload() { async onUpload() {
try { try {
await this.uploadImage({ files: this.currentFiles, url: this.modalUrl }); await this.uploadImage({
files: this.currentFiles,
url: this.modalUrl,
urlText: this.modalUrlText,
});
// Error case handled within action // Error case handled within action
} finally { } finally {
this.clearInputs(); this.clearInputs();
...@@ -66,9 +72,9 @@ export default { ...@@ -66,9 +72,9 @@ export default {
i18n: { i18n: {
modalUpload: __('Upload'), modalUpload: __('Upload'),
modalCancel: __('Cancel'), modalCancel: __('Cancel'),
modalTitle: s__('Incidents|Add a URL'), modalTitle: s__('Incidents|Add image details'),
modalDescription: s__( modalDescription: s__(
'Incidents|You can optionally add a URL to link users to the original graph.', "Incidents|Add text or a link to display with your image. If you don't add either, the file name displays instead.",
), ),
dropDescription: s__( dropDescription: s__(
'Incidents|Drop or %{linkStart}upload%{linkEnd} a metric screenshot to attach it to the incident', 'Incidents|Drop or %{linkStart}upload%{linkEnd} a metric screenshot to attach it to the incident',
...@@ -93,8 +99,12 @@ export default { ...@@ -93,8 +99,12 @@ export default {
@primary.prevent="onUpload" @primary.prevent="onUpload"
> >
<p>{{ $options.i18n.modalDescription }}</p> <p>{{ $options.i18n.modalDescription }}</p>
<gl-form-group :label="__('Text (optional)')" label-for="upload-text-input">
<gl-form-input id="upload-text-input" v-model="modalUrlText" />
</gl-form-group>
<gl-form-group <gl-form-group
:label="__('URL')" :label="__('Link (optional)')"
label-for="upload-url-input" label-for="upload-url-input"
:description="s__('Incidents|Must start with http or https')" :description="s__('Incidents|Must start with http or https')"
> >
......
...@@ -11,6 +11,11 @@ export const uploadMetricImage = async (payload) => { ...@@ -11,6 +11,11 @@ export const uploadMetricImage = async (payload) => {
return convertObjectPropsToCamelCase(response.data); return convertObjectPropsToCamelCase(response.data);
}; };
export const updateMetricImage = async (payload) => {
const response = await Api.updateIssueMetricImage(payload);
return convertObjectPropsToCamelCase(response.data);
};
export const deleteMetricImage = async (payload) => { export const deleteMetricImage = async (payload) => {
const response = await Api.deleteMetricImage(payload); const response = await Api.deleteMetricImage(payload);
return convertObjectPropsToCamelCase(response.data); return convertObjectPropsToCamelCase(response.data);
......
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { deleteMetricImage, getMetricImages, uploadMetricImage } from '../service'; import {
deleteMetricImage,
getMetricImages,
uploadMetricImage,
updateMetricImage,
} from '../service';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const fetchMetricImages = async ({ state, commit }) => { export const fetchMetricImages = async ({ state, commit }) => {
...@@ -17,13 +22,19 @@ export const fetchMetricImages = async ({ state, commit }) => { ...@@ -17,13 +22,19 @@ export const fetchMetricImages = async ({ state, commit }) => {
} }
}; };
export const uploadImage = async ({ state, commit }, { files, url }) => { export const uploadImage = async ({ state, commit }, { files, url, urlText }) => {
commit(types.REQUEST_METRIC_UPLOAD); commit(types.REQUEST_METRIC_UPLOAD);
const { issueIid, projectId } = state; const { issueIid, projectId } = state;
try { try {
const response = await uploadMetricImage({ file: files.item(0), id: projectId, issueIid, url }); const response = await uploadMetricImage({
file: files.item(0),
id: projectId,
issueIid,
url,
urlText,
});
commit(types.RECEIVE_METRIC_UPLOAD_SUCCESS, response); commit(types.RECEIVE_METRIC_UPLOAD_SUCCESS, response);
} catch (error) { } catch (error) {
commit(types.RECEIVE_METRIC_UPLOAD_ERROR); commit(types.RECEIVE_METRIC_UPLOAD_ERROR);
...@@ -31,6 +42,26 @@ export const uploadImage = async ({ state, commit }, { files, url }) => { ...@@ -31,6 +42,26 @@ export const uploadImage = async ({ state, commit }, { files, url }) => {
} }
}; };
export const updateImage = async ({ state, commit }, { imageId, url, urlText }) => {
commit(types.REQUEST_METRIC_UPLOAD);
const { issueIid, projectId } = state;
try {
const response = await updateMetricImage({
issueIid,
id: projectId,
imageId,
url,
urlText,
});
commit(types.RECEIVE_METRIC_UPDATE_SUCCESS, response);
} catch (error) {
commit(types.RECEIVE_METRIC_UPLOAD_ERROR);
createFlash({ message: s__('Incidents|There was an issue updating your image.') });
}
};
export const deleteImage = async ({ state, commit }, imageId) => { export const deleteImage = async ({ state, commit }, imageId) => {
const { issueIid, projectId } = state; const { issueIid, projectId } = state;
......
...@@ -6,6 +6,8 @@ export const REQUEST_METRIC_UPLOAD = 'REQUEST_METRIC_UPLOAD'; ...@@ -6,6 +6,8 @@ export const REQUEST_METRIC_UPLOAD = 'REQUEST_METRIC_UPLOAD';
export const RECEIVE_METRIC_UPLOAD_SUCCESS = 'RECEIVE_METRIC_UPLOAD_SUCCESS'; export const RECEIVE_METRIC_UPLOAD_SUCCESS = 'RECEIVE_METRIC_UPLOAD_SUCCESS';
export const RECEIVE_METRIC_UPLOAD_ERROR = 'RECEIVE_METRIC_UPLOAD_ERROR'; export const RECEIVE_METRIC_UPLOAD_ERROR = 'RECEIVE_METRIC_UPLOAD_ERROR';
export const RECEIVE_METRIC_UPDATE_SUCCESS = 'RECEIVE_METRIC_UPDATE_SUCCESS';
export const RECEIVE_METRIC_DELETE_SUCCESS = 'RECEIVE_METRIC_DELETE_SUCCESS'; export const RECEIVE_METRIC_DELETE_SUCCESS = 'RECEIVE_METRIC_DELETE_SUCCESS';
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
...@@ -21,6 +21,13 @@ export default { ...@@ -21,6 +21,13 @@ export default {
[types.RECEIVE_METRIC_UPLOAD_ERROR](state) { [types.RECEIVE_METRIC_UPLOAD_ERROR](state) {
state.isUploadingImage = false; state.isUploadingImage = false;
}, },
[types.RECEIVE_METRIC_UPDATE_SUCCESS](state, image) {
state.isUploadingImage = false;
const metricIndex = state.metricImages.findIndex((img) => img.id === image.id);
if (metricIndex >= 0) {
state.metricImages.splice(metricIndex, 1, image);
}
},
[types.RECEIVE_METRIC_DELETE_SUCCESS](state, imageId) { [types.RECEIVE_METRIC_DELETE_SUCCESS](state, imageId) {
const metricIndex = state.metricImages.findIndex((image) => image.id === imageId); const metricIndex = state.metricImages.findIndex((image) => image.id === imageId);
state.metricImages.splice(metricIndex, 1); state.metricImages.splice(metricIndex, 1);
......
...@@ -732,12 +732,13 @@ describe('Api', () => { ...@@ -732,12 +732,13 @@ describe('Api', () => {
describe('uploadIssueMetricImage', () => { describe('uploadIssueMetricImage', () => {
const file = 'mock file'; const file = 'mock file';
const url = 'mock url'; const url = 'mock url';
const urlText = 'mock urlText';
it('uploads an image', async () => { it('uploads an image', async () => {
jest.spyOn(axios, 'post'); jest.spyOn(axios, 'post');
mock.onPost(expectedUrl).replyOnce(httpStatus.OK, {}); mock.onPost(expectedUrl).replyOnce(httpStatus.OK, {});
await Api.uploadIssueMetricImage({ issueIid, id: projectId, file, url }).then( await Api.uploadIssueMetricImage({ issueIid, id: projectId, file, url, urlText }).then(
({ data }) => { ({ data }) => {
expect(data).toEqual({}); expect(data).toEqual({});
expect(axios.post.mock.calls[0][2]).toEqual({ expect(axios.post.mock.calls[0][2]).toEqual({
......
...@@ -23,6 +23,43 @@ exports[`Metrics upload item render the metrics image component 1`] = ` ...@@ -23,6 +23,43 @@ exports[`Metrics upload item render the metrics image component 1`] = `
</p> </p>
</gl-modal-stub> </gl-modal-stub>
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
data-testid="metric-image-edit-modal"
dismisslabel="Close"
modalclass=""
modalid="edit-metric-modal"
size="sm"
titletag="h4"
>
<gl-form-group-stub
label="Text (optional)"
label-for="upload-text-input"
labeldescription=""
optionaltext="(optional)"
>
<gl-form-input-stub
data-testid="metric-image-text-field"
id="upload-text-input"
/>
</gl-form-group-stub>
<gl-form-group-stub
description="Must start with http or https"
label="Link (optional)"
label-for="upload-url-input"
labeldescription=""
optionaltext="(optional)"
>
<gl-form-input-stub
data-testid="metric-image-url-field"
id="upload-url-input"
/>
</gl-form-group-stub>
</gl-modal-stub>
<div <div
class="gl-display-flex gl-flex-direction-column" class="gl-display-flex gl-flex-direction-column"
data-testid="metric-image-body" data-testid="metric-image-body"
......
...@@ -47,14 +47,22 @@ describe('Metrics upload item', () => { ...@@ -47,14 +47,22 @@ describe('Metrics upload item', () => {
}); });
const findImageLink = () => wrapper.findComponent(GlLink); const findImageLink = () => wrapper.findComponent(GlLink);
const findLabelTextSpan = () => wrapper.find('[data-testid="metric-image-label-span"]');
const findCollapseButton = () => wrapper.find('[data-testid="collapse-button"]'); const findCollapseButton = () => wrapper.find('[data-testid="collapse-button"]');
const findMetricImageBody = () => wrapper.find('[data-testid="metric-image-body"]'); const findMetricImageBody = () => wrapper.find('[data-testid="metric-image-body"]');
const findModal = () => wrapper.findComponent(GlModal); const findModal = () => wrapper.findComponent(GlModal);
const findEditModal = () => wrapper.find('[data-testid="metric-image-edit-modal"]');
const findDeleteButton = () => wrapper.find('[data-testid="delete-button"]'); const findDeleteButton = () => wrapper.find('[data-testid="delete-button"]');
const findEditButton = () => wrapper.find('[data-testid="edit-button"]');
const findImageTextInput = () => wrapper.find('[data-testid="metric-image-text-field"]');
const findImageUrlInput = () => wrapper.find('[data-testid="metric-image-url-field"]');
const closeModal = () => findModal().vm.$emit('hidden'); const closeModal = () => findModal().vm.$emit('hidden');
const submitModal = () => findModal().vm.$emit('primary', mockEvent); const submitModal = () => findModal().vm.$emit('primary', mockEvent);
const deleteImage = () => findDeleteButton().vm.$emit('click'); const deleteImage = () => findDeleteButton().vm.$emit('click');
const closeEditModal = () => findEditModal().vm.$emit('hidden');
const submitEditModal = () => findEditModal().vm.$emit('primary', mockEvent);
const editImage = () => findEditButton().vm.$emit('click');
it('render the metrics image component', () => { it('render the metrics image component', () => {
mountComponent({}, shallowMount); mountComponent({}, shallowMount);
...@@ -70,6 +78,22 @@ describe('Metrics upload item', () => { ...@@ -70,6 +78,22 @@ describe('Metrics upload item', () => {
expect(findImageLink().text()).toBe(defaultProps.filename); expect(findImageLink().text()).toBe(defaultProps.filename);
}); });
it('shows a link with the url text, if url text is present', () => {
const testUrl = 'test_url';
const testUrlText = 'test_url_text';
mountComponent({ propsData: { url: testUrl, urlText: testUrlText } });
expect(findImageLink().attributes('href')).toBe(testUrl);
expect(findImageLink().text()).toBe(testUrlText);
});
it('shows the url text with no url, if no url is present', () => {
const testUrlText = 'test_url_text';
mountComponent({ propsData: { urlText: testUrlText } });
expect(findLabelTextSpan().text()).toBe(testUrlText);
});
describe('expand and collapse', () => { describe('expand and collapse', () => {
beforeEach(() => { beforeEach(() => {
mountComponent(); mountComponent();
...@@ -89,7 +113,7 @@ describe('Metrics upload item', () => { ...@@ -89,7 +113,7 @@ describe('Metrics upload item', () => {
}); });
describe('delete functionality', () => { describe('delete functionality', () => {
it('should open the modal when clicked', async () => { it('should open the delete modal when clicked', async () => {
mountComponent({ stubs: { GlModal: true } }); mountComponent({ stubs: { GlModal: true } });
deleteImage(); deleteImage();
...@@ -138,4 +162,69 @@ describe('Metrics upload item', () => { ...@@ -138,4 +162,69 @@ describe('Metrics upload item', () => {
}); });
}); });
}); });
describe('edit functionality', () => {
it('should open the delete modal when clicked', async () => {
mountComponent({ stubs: { GlModal: true } });
editImage();
await waitForPromises();
expect(findEditModal().attributes('visible')).toBe('true');
});
describe('when the modal is open', () => {
beforeEach(() => {
mountComponent({
data() {
return { editModalVisible: true };
},
propsData: { urlText: 'test' },
stubs: { GlModal: true },
});
});
it('should close the modal when cancelled', async () => {
closeEditModal();
await waitForPromises();
expect(findEditModal().attributes('visible')).toBeFalsy();
});
it('should delete the image when selected', async () => {
const dispatchSpy = jest.spyOn(store, 'dispatch').mockImplementation(jest.fn());
submitEditModal();
await waitForPromises();
expect(dispatchSpy).toHaveBeenCalledWith('updateImage', {
imageId: defaultProps.id,
url: null,
urlText: 'test',
});
});
it('should clear edits when the modal is closed', async () => {
await findImageTextInput().setValue('test value');
await findImageUrlInput().setValue('http://www.gitlab.com');
expect(findImageTextInput().element.value).toBe('test value');
expect(findImageUrlInput().element.value).toBe('http://www.gitlab.com');
closeEditModal();
await waitForPromises();
editImage();
await waitForPromises();
expect(findImageTextInput().element.value).toBe('test');
expect(findImageUrlInput().element.value).toBe('');
});
});
});
}); });
...@@ -131,7 +131,11 @@ describe('Metrics tab', () => { ...@@ -131,7 +131,11 @@ describe('Metrics tab', () => {
await waitForPromises(); await waitForPromises();
expect(dispatchSpy).toHaveBeenCalledWith('uploadImage', { files: fileList, url: testUrl }); expect(dispatchSpy).toHaveBeenCalledWith('uploadImage', {
files: fileList,
url: testUrl,
urlText: '',
});
}); });
describe('url field', () => { describe('url field', () => {
...@@ -144,7 +148,11 @@ describe('Metrics tab', () => { ...@@ -144,7 +148,11 @@ describe('Metrics tab', () => {
}); });
it('should display the url field', () => { it('should display the url field', () => {
expect(wrapper.findComponent(GlFormInput).attributes('value')).toBe(testUrl); expect(wrapper.find('#upload-url-input').attributes('value')).toBe(testUrl);
});
it('should display the url text field', () => {
expect(wrapper.find('#upload-text-input').attributes('value')).toBe('');
}); });
it('should clear url when cancelled', async () => { it('should clear url when cancelled', async () => {
......
import Api from 'ee/api'; import Api from 'ee/api';
import { getMetricImages, uploadMetricImage } from 'ee/issues/show/components/incidents/service'; import {
getMetricImages,
uploadMetricImage,
updateMetricImage,
} from 'ee/issues/show/components/incidents/service';
import { fileList, fileListRaw } from './mock_data'; import { fileList, fileListRaw } from './mock_data';
jest.mock('ee/api', () => ({ jest.mock('ee/api', () => ({
fetchIssueMetricImages: jest.fn(), fetchIssueMetricImages: jest.fn(),
uploadIssueMetricImage: jest.fn(), uploadIssueMetricImage: jest.fn(),
updateIssueMetricImage: jest.fn(),
})); }));
describe('Incidents service', () => { describe('Incidents service', () => {
...@@ -23,4 +28,12 @@ describe('Incidents service', () => { ...@@ -23,4 +28,12 @@ describe('Incidents service', () => {
expect(Api.uploadIssueMetricImage).toHaveBeenCalled(); expect(Api.uploadIssueMetricImage).toHaveBeenCalled();
expect(result).toEqual(fileList[0]); expect(result).toEqual(fileList[0]);
}); });
it('updates a metric image', async () => {
Api.updateIssueMetricImage.mockResolvedValue({ data: fileListRaw[0] });
const result = await updateMetricImage();
expect(Api.updateIssueMetricImage).toHaveBeenCalled();
expect(result).toEqual(fileList[0]);
});
}); });
...@@ -3,6 +3,7 @@ import Vuex from 'vuex'; ...@@ -3,6 +3,7 @@ import Vuex from 'vuex';
import { import {
getMetricImages, getMetricImages,
uploadMetricImage, uploadMetricImage,
updateMetricImage,
deleteMetricImage, deleteMetricImage,
} from 'ee/issues/show/components/incidents/service'; } from 'ee/issues/show/components/incidents/service';
import createStore from 'ee/issues/show/components/incidents/store'; import createStore from 'ee/issues/show/components/incidents/store';
...@@ -17,6 +18,7 @@ jest.mock('~/flash'); ...@@ -17,6 +18,7 @@ jest.mock('~/flash');
jest.mock('ee/issues/show/components/incidents/service', () => ({ jest.mock('ee/issues/show/components/incidents/service', () => ({
getMetricImages: jest.fn(), getMetricImages: jest.fn(),
uploadMetricImage: jest.fn(), uploadMetricImage: jest.fn(),
updateMetricImage: jest.fn(),
deleteMetricImage: jest.fn(), deleteMetricImage: jest.fn(),
})); }));
...@@ -104,6 +106,37 @@ describe('Metrics tab store actions', () => { ...@@ -104,6 +106,37 @@ describe('Metrics tab store actions', () => {
}); });
}); });
describe('updating metric images', () => {
const payload = {
url: 'test_url',
urlText: 'url text',
};
it('should call success action when updating an image', () => {
updateMetricImage.mockImplementation(() => Promise.resolve());
testAction(actions.updateImage, payload, state, [
{ type: types.REQUEST_METRIC_UPLOAD },
{
type: types.RECEIVE_METRIC_UPDATE_SUCCESS,
},
]);
});
it('should call error action when failing to update an image', async () => {
updateMetricImage.mockImplementation(() => Promise.reject());
await testAction(
actions.updateImage,
payload,
state,
[{ type: types.REQUEST_METRIC_UPLOAD }, { type: types.RECEIVE_METRIC_UPLOAD_ERROR }],
[],
);
expect(createFlash).toHaveBeenCalled();
});
});
describe('deleting a metric image', () => { describe('deleting a metric image', () => {
const payload = fileList[0].id; const payload = fileList[0].id;
......
...@@ -101,6 +101,25 @@ describe('Metric images mutations', () => { ...@@ -101,6 +101,25 @@ describe('Metric images mutations', () => {
}); });
}); });
describe('RECEIVE_METRIC_UPDATE_SUCCESS', () => {
const initialImage = testImages[0];
const newImage = testImages[0];
newImage.url = 'https://www.gitlab.com';
beforeEach(() => {
createState({ metricImages: [initialImage] });
mutations[types.RECEIVE_METRIC_UPDATE_SUCCESS](state, newImage);
});
it('should unset the loading state', () => {
expect(state.isUploadingImage).toBe(false);
});
it('should replace the existing image with the new one', () => {
expect(state.metricImages).toMatchObject([newImage]);
});
});
describe('RECEIVE_METRIC_DELETE_SUCCESS', () => { describe('RECEIVE_METRIC_DELETE_SUCCESS', () => {
const deletedImageId = testImages[1].id; const deletedImageId = testImages[1].id;
const expectedResult = [testImages[0], testImages[2]]; const expectedResult = [testImages[0], testImages[2]];
......
...@@ -19184,7 +19184,10 @@ msgstr "" ...@@ -19184,7 +19184,10 @@ msgstr ""
msgid "Incidents" msgid "Incidents"
msgstr "" msgstr ""
msgid "Incidents|Add a URL" msgid "Incidents|Add image details"
msgstr ""
msgid "Incidents|Add text or a link to display with your image. If you don't add either, the file name displays instead."
msgstr "" msgstr ""
msgid "Incidents|Drop or %{linkStart}upload%{linkEnd} a metric screenshot to attach it to the incident" msgid "Incidents|Drop or %{linkStart}upload%{linkEnd} a metric screenshot to attach it to the incident"
...@@ -19199,10 +19202,10 @@ msgstr "" ...@@ -19199,10 +19202,10 @@ msgstr ""
msgid "Incidents|There was an issue loading metric images." msgid "Incidents|There was an issue loading metric images."
msgstr "" msgstr ""
msgid "Incidents|There was an issue uploading your image." msgid "Incidents|There was an issue updating your image."
msgstr "" msgstr ""
msgid "Incidents|You can optionally add a URL to link users to the original graph." msgid "Incidents|There was an issue uploading your image."
msgstr "" msgstr ""
msgid "Incident|Alert details" msgid "Incident|Alert details"
...@@ -19211,9 +19214,18 @@ msgstr "" ...@@ -19211,9 +19214,18 @@ msgstr ""
msgid "Incident|Are you sure you wish to delete this image?" msgid "Incident|Are you sure you wish to delete this image?"
msgstr "" msgstr ""
msgid "Incident|Delete image"
msgstr ""
msgid "Incident|Deleting %{filename}" msgid "Incident|Deleting %{filename}"
msgstr "" msgstr ""
msgid "Incident|Edit image text or link"
msgstr ""
msgid "Incident|Editing %{filename}"
msgstr ""
msgid "Incident|Metrics" msgid "Incident|Metrics"
msgstr "" msgstr ""
...@@ -21740,6 +21752,9 @@ msgstr "" ...@@ -21740,6 +21752,9 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "" msgstr ""
msgid "Link (optional)"
msgstr ""
msgid "Link Prometheus monitoring to GitLab." msgid "Link Prometheus monitoring to GitLab."
msgstr "" msgstr ""
...@@ -35999,6 +36014,9 @@ msgstr "" ...@@ -35999,6 +36014,9 @@ msgstr ""
msgid "Tests" msgid "Tests"
msgstr "" msgstr ""
msgid "Text (optional)"
msgstr ""
msgid "Text added to the body of all email messages. %{character_limit} character limit" msgid "Text added to the body of all email messages. %{character_limit} character limit"
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