Commit 2b9917ea authored by Scott Hampton's avatar Scott Hampton Committed by Kushal Pandya

Add changed pages to visual review modal

- Pass the changed pages list to the visual review link
- Show the dropdown of changed pages if there are some
- Fix the modal to not mention a step 5, since that was removed
- Fix a parent prop being passed to match the child prop name
parent 7184b409
......@@ -64,7 +64,7 @@ export default {
:deployment="deployment"
:computed-deployment-status="computedDeploymentStatus"
:show-visual-review-app="showVisualReviewApp"
:visual-review-app-metadata="visualReviewAppMeta"
:visual-review-app-meta="visualReviewAppMeta"
/>
</div>
</div>
......
......@@ -173,7 +173,7 @@ export default {
:app-button-text="appButtonText"
:deployment="deployment"
:show-visual-review-app="showVisualReviewApp"
:visual-review-app-metadata="visualReviewAppMeta"
:visual-review-app-meta="visualReviewAppMeta"
/>
<deployment-action-button
v-if="stopUrl"
......
......@@ -93,8 +93,10 @@ export default {
/>
<visual-review-app-link
v-if="showVisualReviewApp"
:view-app-display="appButtonText"
:link="deploymentExternalUrl"
:app-metadata="visualReviewAppMeta"
:changes="deployment.changes"
/>
</span>
</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)
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,
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)
......
<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 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';
export default {
components: {
GlButton,
GlDropdown,
GlDropdownItem,
GlModal,
GlSearchBoxByType,
Icon,
ReviewAppLink,
ModalCopyButton,
},
directives: {
......@@ -19,6 +31,11 @@ export default {
type: Object,
required: true,
},
changes: {
type: Array,
required: false,
default: () => [],
},
cssClass: {
type: String,
required: false,
......@@ -28,10 +45,15 @@ export default {
type: String,
required: true,
},
viewAppDisplay: {
type: Object,
required: true,
},
},
data() {
return {
modalId: 'visual-review-app-info',
changesSearchTerm: '',
};
},
computed: {
......@@ -55,6 +77,9 @@ export default {
};
/* eslint-enable no-useless-escape */
},
filteredChanges() {
return this.changes.filter(change => change.path.includes(this.changesSearchTerm));
},
instructionText() {
return {
intro: {
......@@ -62,7 +87,7 @@ export default {
'VisualReviewApp|Follow the steps below to enable Visual Reviews inside your application.',
),
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(
......@@ -111,6 +136,20 @@ export default {
modalTitle() {
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>
......@@ -125,25 +164,14 @@ export default {
{{ s__('VisualReviewApp|Review') }}
</gl-button>
<gl-modal
ref="modal"
:modal-id="modalId"
:title="modalTitle"
lazy
static
size="lg"
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.p2"></p>
<div>
......@@ -169,6 +197,45 @@ export default {
/>
</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>
</div>
</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 { GlButton, GlModal } from '@gitlab/ui';
import { GlButton, GlDropdown, GlModal } from '@gitlab/ui';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
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', () => {
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(() => {
propsData = {
cssClass: 'button cool-button best-button',
appMetadata: {
mergeRequestId: 1,
sourceProjectId: 20,
appUrl: 'http://gitlab.example.com',
sourceProjectPath: 'source/project',
},
link: 'http://example.com',
};
factory({
propsData,
});
});
afterEach(() => {
......@@ -26,12 +45,6 @@ describe('Visual Review App Link', () => {
});
describe('renders link and text', () => {
beforeEach(() => {
wrapper = mount(VisualReviewAppLink, {
propsData,
});
});
it('renders Review text', () => {
expect(wrapper.find(GlButton).text()).toBe('Review');
});
......@@ -45,62 +58,105 @@ describe('Visual Review App Link', () => {
describe('renders the modal', () => {
beforeEach(() => {
wrapper = shallowMount(VisualReviewAppLink, {
propsData,
});
openModal();
});
it('with expected project Id', () => {
expect(wrapper.find(GlModal).text()).toEqual(
expect(findModal().text()).toEqual(
expect.stringContaining(`data-project-id='${propsData.appMetadata.sourceProjectId}'`),
);
});
it('with expected project path', () => {
expect(wrapper.find(GlModal).text()).toEqual(
expect(findModal().text()).toEqual(
expect.stringContaining(`data-project-path='${propsData.appMetadata.sourceProjectPath}'`),
);
});
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}'`),
);
});
it('with expected appUrl', () => {
expect(wrapper.find(GlModal).text()).toEqual(
expect(findModal().text()).toEqual(
expect.stringContaining(`data-mr-url='${propsData.appMetadata.appUrl}'`),
);
});
it('with review app link', () => {
expect(
wrapper
.find(GlModal)
.find('a.js-review-app-link')
.attributes('href'),
).toEqual(propsData.link);
describe('renders the copyToClipboard button', () => {
it('within the modal', () => {
expect(wrapper.find(ModalCopyButton).exists()).toEqual(true);
});
it('with the expected modalId', () => {
const { modalId } = findModal().props();
expect(wrapper.find(ModalCopyButton).props().modalId).toBe(modalId);
});
});
it('tracks an event when review app link is clicked', () => {
const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
const appLink = wrapper.find(GlModal).find('a.js-review-app-link');
triggerEvent(appLink.element);
describe('renders modal footer', () => {
describe('when no changes are listed', () => {
it('with review app link', () => {
expect(wrapper.find('a.js-deploy-url').attributes('href')).toEqual(propsData.link);
});
expect(spy).toHaveBeenCalledWith('_category_', 'open_review_app', {
label: 'review_app',
it('tracks an event when review app link is clicked', () => {
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', () => {
it('within the modal', () => {
expect(wrapper.find(ModalCopyButton)).toBeTruthy();
});
describe('when changes are listed', () => {
beforeEach(() => {
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', () => {
const renderedId = wrapper.find(GlModal).attributes('modalid');
expect(wrapper.find(ModalCopyButton).props().modalId).toBe(renderedId);
it('tracks an event when review app link is clicked', () => {
const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
const appLink = findModal().find(`a[href='${propsData.link}/example-path']`);
triggerEvent(appLink.element);
expect(spy).toHaveBeenCalledWith('_category_', 'open_review_app', {
label: 'review_app',
});
});
});
});
});
});
......@@ -22140,6 +22140,9 @@ msgstr ""
msgid "VisualReviewApp|%{stepStart}Step 4%{stepEnd}. Leave feedback in the Review App."
msgstr ""
msgid "VisualReviewApp|Cancel"
msgstr ""
msgid "VisualReviewApp|Copy merge request ID"
msgstr ""
......@@ -22152,13 +22155,16 @@ msgstr ""
msgid "VisualReviewApp|Follow the steps below to enable Visual Reviews inside your application."
msgstr ""
msgid "VisualReviewApp|No review app found or available."
msgstr ""
msgid "VisualReviewApp|Open review app"
msgstr ""
msgid "VisualReviewApp|Review"
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 ""
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