Commit c39a3841 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '27227-widget-showing-changed-pages-for-visual-reviews' into 'master'

Add changed pages to visual review modal

Closes #27227

See merge request gitlab-org/gitlab!26620
parents b204125c 2b9917ea
...@@ -64,7 +64,7 @@ export default { ...@@ -64,7 +64,7 @@ export default {
:deployment="deployment" :deployment="deployment"
:computed-deployment-status="computedDeploymentStatus" :computed-deployment-status="computedDeploymentStatus"
:show-visual-review-app="showVisualReviewApp" :show-visual-review-app="showVisualReviewApp"
:visual-review-app-metadata="visualReviewAppMeta" :visual-review-app-meta="visualReviewAppMeta"
/> />
</div> </div>
</div> </div>
......
...@@ -173,7 +173,7 @@ export default { ...@@ -173,7 +173,7 @@ export default {
:app-button-text="appButtonText" :app-button-text="appButtonText"
:deployment="deployment" :deployment="deployment"
:show-visual-review-app="showVisualReviewApp" :show-visual-review-app="showVisualReviewApp"
:visual-review-app-metadata="visualReviewAppMeta" :visual-review-app-meta="visualReviewAppMeta"
/> />
<deployment-action-button <deployment-action-button
v-if="stopUrl" v-if="stopUrl"
......
...@@ -93,8 +93,10 @@ export default { ...@@ -93,8 +93,10 @@ export default {
/> />
<visual-review-app-link <visual-review-app-link
v-if="showVisualReviewApp" v-if="showVisualReviewApp"
:view-app-display="appButtonText"
:link="deploymentExternalUrl" :link="deploymentExternalUrl"
:app-metadata="visualReviewAppMeta" :app-metadata="visualReviewAppMeta"
:changes="deployment.changes"
/> />
</span> </span>
</template> </template>
---
title: Add changed pages dropdown to visual review modal
merge_request:
author:
type: added
...@@ -200,7 +200,8 @@ Feature.enable(:anonymous_visual_review_feedback) ...@@ -200,7 +200,8 @@ Feature.enable(:anonymous_visual_review_feedback)
The feedback form is served through a script you add to pages in your Review App. The feedback form is served through a script you add to pages in your Review App.
If you have [Developer permissions](../../user/permissions.md) to the project, If you have [Developer permissions](../../user/permissions.md) to the project,
you can access it by clicking the **Review** button in the **Pipeline** section you can access it by clicking the **Review** button in the **Pipeline** section
of the merge request. of the merge request. The form modal will also show a dropdown for changed pages
if [route maps](#route-maps) are configured in the project.
![review button](img/review_button.png) ![review button](img/review_button.png)
......
<script> <script>
import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; import {
GlButton,
GlDropdown,
GlDropdownItem,
GlModal,
GlSearchBoxByType,
GlModalDirective,
} from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
export default { export default {
components: { components: {
GlButton, GlButton,
GlDropdown,
GlDropdownItem,
GlModal, GlModal,
GlSearchBoxByType,
Icon, Icon,
ReviewAppLink,
ModalCopyButton, ModalCopyButton,
}, },
directives: { directives: {
...@@ -19,6 +31,11 @@ export default { ...@@ -19,6 +31,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
changes: {
type: Array,
required: false,
default: () => [],
},
cssClass: { cssClass: {
type: String, type: String,
required: false, required: false,
...@@ -28,10 +45,15 @@ export default { ...@@ -28,10 +45,15 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
viewAppDisplay: {
type: Object,
required: true,
},
}, },
data() { data() {
return { return {
modalId: 'visual-review-app-info', modalId: 'visual-review-app-info',
changesSearchTerm: '',
}; };
}, },
computed: { computed: {
...@@ -55,6 +77,9 @@ export default { ...@@ -55,6 +77,9 @@ export default {
}; };
/* eslint-enable no-useless-escape */ /* eslint-enable no-useless-escape */
}, },
filteredChanges() {
return this.changes.filter(change => change.path.includes(this.changesSearchTerm));
},
instructionText() { instructionText() {
return { return {
intro: { intro: {
...@@ -62,7 +87,7 @@ export default { ...@@ -62,7 +87,7 @@ export default {
'VisualReviewApp|Follow the steps below to enable Visual Reviews inside your application.', 'VisualReviewApp|Follow the steps below to enable Visual Reviews inside your application.',
), ),
p2: s__( p2: s__(
'VisualReviewApp|Steps 1 and 2 (and sometimes 3) are performed once by the developer before requesting feedback. Steps 3 (if necessary), 4, and 5 are performed by the reviewer each time they perform a review.', 'VisualReviewApp|Steps 1 and 2 (and sometimes 3) are performed once by the developer before requesting feedback. Steps 3 (if necessary), 4 is performed by the reviewer each time they perform a review.',
), ),
}, },
step1: sprintf( step1: sprintf(
...@@ -111,6 +136,20 @@ export default { ...@@ -111,6 +136,20 @@ export default {
modalTitle() { modalTitle() {
return s__('VisualReviewApp|Enable Visual Reviews'); return s__('VisualReviewApp|Enable Visual Reviews');
}, },
shouldShowChanges() {
return this.changes.length > 0;
},
isSearchEmpty() {
return this.filteredChanges.length === 0;
},
},
methods: {
cancel() {
this.$refs.modal.cancel();
},
ok() {
this.$refs.modal.ok();
},
}, },
}; };
</script> </script>
...@@ -125,25 +164,14 @@ export default { ...@@ -125,25 +164,14 @@ export default {
{{ s__('VisualReviewApp|Review') }} {{ s__('VisualReviewApp|Review') }}
</gl-button> </gl-button>
<gl-modal <gl-modal
ref="modal"
:modal-id="modalId" :modal-id="modalId"
:title="modalTitle" :title="modalTitle"
lazy
static
size="lg" size="lg"
class="text-2 ws-normal" class="text-2 ws-normal"
ok-variant="success"
> >
<template slot="modal-ok">
<a
:href="link"
target="_blank"
rel="noopener noreferrer nofollow"
class="text-white js-review-app-link"
data-track-event="open_review_app"
data-track-label="review_app"
>
{{ s__('VisualReviewApp|Open review app') }}
<icon class="fwhite" name="external-link" />
</a>
</template>
<p v-html="instructionText.intro.p1"></p> <p v-html="instructionText.intro.p1"></p>
<p v-html="instructionText.intro.p2"></p> <p v-html="instructionText.intro.p2"></p>
<div> <div>
...@@ -169,6 +197,45 @@ export default { ...@@ -169,6 +197,45 @@ export default {
/> />
</p> </p>
<p v-html="instructionText.step4"></p> <p v-html="instructionText.step4"></p>
<template #modal-footer>
<gl-button @click="cancel">
{{ s__('VisualReviewApp|Cancel') }}
</gl-button>
<gl-dropdown
v-if="shouldShowChanges"
dropup
right
split
:split-href="link"
data-track-event="open_review_app"
data-track-label="review_app"
@click="ok"
>
<gl-search-box-by-type v-model.trim="changesSearchTerm" class="m-2" />
<template #button-content>
{{ s__('VisualReviewApp|Open review app') }}
<icon class="fgray" name="external-link" />
</template>
<gl-dropdown-item
v-for="change in filteredChanges"
:key="change.path"
:href="change.external_url"
data-track-event="open_review_app"
data-track-label="review_app"
>{{ change.path }}</gl-dropdown-item
>
<div v-show="isSearchEmpty" class="text-secondary p-2">
{{ s__('VisualReviewApp|No review app found or available.') }}
</div>
</gl-dropdown>
<review-app-link
v-else
:display="viewAppDisplay"
:link="link"
css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
/>
</template>
</gl-modal> </gl-modal>
</div> </div>
</template> </template>
import { shallowMount, mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import VisualReviewAppLink from 'ee/vue_merge_request_widget/components/visual_review_app_link.vue'; import VisualReviewAppLink from 'ee/vue_merge_request_widget/components/visual_review_app_link.vue';
import { GlButton, GlModal } from '@gitlab/ui'; import { GlButton, GlDropdown, GlModal } from '@gitlab/ui';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper'; import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
const propsData = {
cssClass: 'button cool-button best-button',
appMetadata: {
mergeRequestId: 1,
sourceProjectId: 20,
appUrl: 'http://gitlab.example.com',
sourceProjectPath: 'source/project',
},
viewAppDisplay: {
text: 'View app',
tooltip: '',
},
link: 'http://example.com',
};
describe('Visual Review App Link', () => { describe('Visual Review App Link', () => {
let wrapper; let wrapper;
let propsData;
const factory = (options = {}) => {
wrapper = mount(VisualReviewAppLink, {
...options,
});
};
const openModal = () => {
wrapper.find('.js-review-button').trigger('click');
};
const findModal = () => wrapper.find(GlModal);
beforeEach(() => { beforeEach(() => {
propsData = { factory({
cssClass: 'button cool-button best-button', propsData,
appMetadata: { });
mergeRequestId: 1,
sourceProjectId: 20,
appUrl: 'http://gitlab.example.com',
sourceProjectPath: 'source/project',
},
link: 'http://example.com',
};
}); });
afterEach(() => { afterEach(() => {
...@@ -26,12 +45,6 @@ describe('Visual Review App Link', () => { ...@@ -26,12 +45,6 @@ describe('Visual Review App Link', () => {
}); });
describe('renders link and text', () => { describe('renders link and text', () => {
beforeEach(() => {
wrapper = mount(VisualReviewAppLink, {
propsData,
});
});
it('renders Review text', () => { it('renders Review text', () => {
expect(wrapper.find(GlButton).text()).toBe('Review'); expect(wrapper.find(GlButton).text()).toBe('Review');
}); });
...@@ -45,62 +58,105 @@ describe('Visual Review App Link', () => { ...@@ -45,62 +58,105 @@ describe('Visual Review App Link', () => {
describe('renders the modal', () => { describe('renders the modal', () => {
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(VisualReviewAppLink, { openModal();
propsData,
});
}); });
it('with expected project Id', () => { it('with expected project Id', () => {
expect(wrapper.find(GlModal).text()).toEqual( expect(findModal().text()).toEqual(
expect.stringContaining(`data-project-id='${propsData.appMetadata.sourceProjectId}'`), expect.stringContaining(`data-project-id='${propsData.appMetadata.sourceProjectId}'`),
); );
}); });
it('with expected project path', () => { it('with expected project path', () => {
expect(wrapper.find(GlModal).text()).toEqual( expect(findModal().text()).toEqual(
expect.stringContaining(`data-project-path='${propsData.appMetadata.sourceProjectPath}'`), expect.stringContaining(`data-project-path='${propsData.appMetadata.sourceProjectPath}'`),
); );
}); });
it('with expected merge request id', () => { it('with expected merge request id', () => {
expect(wrapper.find(GlModal).text()).toEqual( expect(findModal().text()).toEqual(
expect.stringContaining(`data-merge-request-id='${propsData.appMetadata.mergeRequestId}'`), expect.stringContaining(`data-merge-request-id='${propsData.appMetadata.mergeRequestId}'`),
); );
}); });
it('with expected appUrl', () => { it('with expected appUrl', () => {
expect(wrapper.find(GlModal).text()).toEqual( expect(findModal().text()).toEqual(
expect.stringContaining(`data-mr-url='${propsData.appMetadata.appUrl}'`), expect.stringContaining(`data-mr-url='${propsData.appMetadata.appUrl}'`),
); );
}); });
it('with review app link', () => { describe('renders the copyToClipboard button', () => {
expect( it('within the modal', () => {
wrapper expect(wrapper.find(ModalCopyButton).exists()).toEqual(true);
.find(GlModal) });
.find('a.js-review-app-link')
.attributes('href'), it('with the expected modalId', () => {
).toEqual(propsData.link); const { modalId } = findModal().props();
expect(wrapper.find(ModalCopyButton).props().modalId).toBe(modalId);
});
}); });
it('tracks an event when review app link is clicked', () => { describe('renders modal footer', () => {
const spy = mockTracking('_category_', wrapper.element, jest.spyOn); describe('when no changes are listed', () => {
const appLink = wrapper.find(GlModal).find('a.js-review-app-link'); it('with review app link', () => {
triggerEvent(appLink.element); expect(wrapper.find('a.js-deploy-url').attributes('href')).toEqual(propsData.link);
});
expect(spy).toHaveBeenCalledWith('_category_', 'open_review_app', { it('tracks an event when review app link is clicked', () => {
label: 'review_app', const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
const appLink = findModal().find('a.js-deploy-url');
triggerEvent(appLink.element);
expect(spy).toHaveBeenCalledWith('_category_', 'open_review_app', {
label: 'review_app',
});
});
}); });
});
});
describe('renders the copyToClipboard button', () => { describe('when changes are listed', () => {
it('within the modal', () => { beforeEach(() => {
expect(wrapper.find(ModalCopyButton)).toBeTruthy(); factory({
}); propsData: {
...propsData,
changes: [
{
path: '/example-path',
external_url: `${propsData.link}/example-path`,
},
],
},
});
openModal();
});
it('with review app split dropdown', () => {
expect(
wrapper
.find(GlDropdown)
.find(`a[href='${propsData.link}']`)
.exists(),
).toEqual(true);
});
it('contains a list of changed pages', () => {
expect(
wrapper
.find(GlDropdown)
.find(`a[href='${propsData.link}/example-path']`)
.exists(),
).toEqual(true);
});
it('with the expected modalId', () => { it('tracks an event when review app link is clicked', () => {
const renderedId = wrapper.find(GlModal).attributes('modalid'); const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
expect(wrapper.find(ModalCopyButton).props().modalId).toBe(renderedId); const appLink = findModal().find(`a[href='${propsData.link}/example-path']`);
triggerEvent(appLink.element);
expect(spy).toHaveBeenCalledWith('_category_', 'open_review_app', {
label: 'review_app',
});
});
});
}); });
}); });
}); });
...@@ -22146,6 +22146,9 @@ msgstr "" ...@@ -22146,6 +22146,9 @@ msgstr ""
msgid "VisualReviewApp|%{stepStart}Step 4%{stepEnd}. Leave feedback in the Review App." msgid "VisualReviewApp|%{stepStart}Step 4%{stepEnd}. Leave feedback in the Review App."
msgstr "" msgstr ""
msgid "VisualReviewApp|Cancel"
msgstr ""
msgid "VisualReviewApp|Copy merge request ID" msgid "VisualReviewApp|Copy merge request ID"
msgstr "" msgstr ""
...@@ -22158,13 +22161,16 @@ msgstr "" ...@@ -22158,13 +22161,16 @@ msgstr ""
msgid "VisualReviewApp|Follow the steps below to enable Visual Reviews inside your application." msgid "VisualReviewApp|Follow the steps below to enable Visual Reviews inside your application."
msgstr "" msgstr ""
msgid "VisualReviewApp|No review app found or available."
msgstr ""
msgid "VisualReviewApp|Open review app" msgid "VisualReviewApp|Open review app"
msgstr "" msgstr ""
msgid "VisualReviewApp|Review" msgid "VisualReviewApp|Review"
msgstr "" msgstr ""
msgid "VisualReviewApp|Steps 1 and 2 (and sometimes 3) are performed once by the developer before requesting feedback. Steps 3 (if necessary), 4, and 5 are performed by the reviewer each time they perform a review." msgid "VisualReviewApp|Steps 1 and 2 (and sometimes 3) are performed once by the developer before requesting feedback. Steps 3 (if necessary), 4 is performed by the reviewer each time they perform a review."
msgstr "" msgstr ""
msgid "Vulnerabilities" msgid "Vulnerabilities"
......
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