Commit 6071b0e9 authored by Andrew Fontaine's avatar Andrew Fontaine

Add new environment and review app buttons back

The new environments page was lacking the new environment and add review
apps buttons.

It has been tweaked slightly to take advantage of the action primary and
secondary props of the GlTabs, which means I can't take advantage of the
GlModalDirective anymore. Instead, I modify the modal to work via
v-model to show/hide the modal. This means that the adjustment for the
mobile view happens in the GlTabs component now, thereby making the app
component template smaller.
parent 9aadeffd
...@@ -12,11 +12,20 @@ export default { ...@@ -12,11 +12,20 @@ export default {
ModalCopyButton, ModalCopyButton,
}, },
inject: ['defaultBranchName'], inject: ['defaultBranchName'],
model: {
prop: 'visible',
event: 'change',
},
props: { props: {
modalId: { modalId: {
type: String, type: String,
required: true, required: true,
}, },
visible: {
type: Boolean,
required: false,
default: false,
},
}, },
instructionText: { instructionText: {
step1: s__( step1: s__(
...@@ -57,12 +66,15 @@ export default { ...@@ -57,12 +66,15 @@ export default {
</script> </script>
<template> <template>
<gl-modal <gl-modal
:visible="visible"
:modal-id="modalId" :modal-id="modalId"
:title="$options.modalInfo.title" :title="$options.modalInfo.title"
static
size="lg" size="lg"
ok-only ok-only
ok-variant="light" ok-variant="light"
:ok-title="$options.modalInfo.closeText" :ok-title="$options.modalInfo.closeText"
@change="$emit('change', $event)"
> >
<p> <p>
<gl-sprintf :message="$options.instructionText.step1"> <gl-sprintf :message="$options.instructionText.step1">
......
<script> <script>
import { GlBadge, GlTab, GlTabs } from '@gitlab/ui'; import { GlBadge, GlTab, GlTabs } from '@gitlab/ui';
import { s__ } from '~/locale';
import environmentAppQuery from '../graphql/queries/environment_app.query.graphql'; import environmentAppQuery from '../graphql/queries/environment_app.query.graphql';
import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql'; import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql';
import EnvironmentFolder from './new_environment_folder.vue'; import EnvironmentFolder from './new_environment_folder.vue';
import EnableReviewAppModal from './enable_review_app_modal.vue';
export default { export default {
components: { components: {
EnvironmentFolder, EnvironmentFolder,
EnableReviewAppModal,
GlBadge, GlBadge,
GlTab, GlTab,
GlTabs, GlTabs,
...@@ -22,22 +25,73 @@ export default { ...@@ -22,22 +25,73 @@ export default {
query: pollIntervalQuery, query: pollIntervalQuery,
}, },
}, },
inject: ['newEnvironmentPath', 'canCreateEnvironment'],
i18n: {
newEnvironmentButtonLabel: s__('Environments|New environment'),
reviewAppButtonLabel: s__('Environments|Enable review app'),
},
modalId: 'enable-review-app-info',
data() { data() {
return { interval: undefined }; return { interval: undefined, isReviewAppModalVisible: false };
}, },
computed: { computed: {
canSetupReviewApp() {
return this.environmentApp?.reviewApp?.canSetupReviewApp;
},
folders() { folders() {
return this.environmentApp?.environments.filter((e) => e.size > 1) ?? []; return this.environmentApp?.environments.filter((e) => e.size > 1) ?? [];
}, },
availableCount() { availableCount() {
return this.environmentApp?.availableCount; return this.environmentApp?.availableCount;
}, },
addEnvironment() {
if (!this.canCreateEnvironment) {
return null;
}
return {
text: this.$options.i18n.newEnvironmentButtonLabel,
attributes: {
href: this.newEnvironmentPath,
category: 'primary',
variant: 'confirm',
},
};
},
openReviewAppModal() {
if (!this.canSetupReviewApp) {
return null;
}
return {
text: this.$options.i18n.reviewAppButtonLabel,
attributes: {
category: 'secondary',
variant: 'confirm',
},
};
},
},
methods: {
showReviewAppModal() {
this.isReviewAppModalVisible = true;
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<gl-tabs> <enable-review-app-modal
v-if="canSetupReviewApp"
v-model="isReviewAppModalVisible"
:modal-id="$options.modalId"
data-testid="enable-review-app-modal"
/>
<gl-tabs
:action-secondary="addEnvironment"
:action-primary="openReviewAppModal"
@primary="showReviewAppModal"
>
<gl-tab> <gl-tab>
<template #title> <template #title>
<span>{{ __('Available') }}</span> <span>{{ __('Available') }}</span>
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import EnableReviewAppButton from '~/environments/components/enable_review_app_modal.vue'; import EnableReviewAppButton from '~/environments/components/enable_review_app_modal.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
describe('Enable Review App Button', () => { describe('Enable Review App Button', () => {
let wrapper; let wrapper;
let modal;
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -16,12 +18,15 @@ describe('Enable Review App Button', () => { ...@@ -16,12 +18,15 @@ describe('Enable Review App Button', () => {
shallowMount(EnableReviewAppButton, { shallowMount(EnableReviewAppButton, {
propsData: { propsData: {
modalId: 'fake-id', modalId: 'fake-id',
visible: true,
}, },
provide: { provide: {
defaultBranchName: 'main', defaultBranchName: 'main',
}, },
}), }),
); );
modal = wrapper.findComponent(GlModal);
}); });
it('renders the defaultBranchName copy', () => { it('renders the defaultBranchName copy', () => {
...@@ -32,5 +37,15 @@ describe('Enable Review App Button', () => { ...@@ -32,5 +37,15 @@ describe('Enable Review App Button', () => {
it('renders the copyToClipboard button', () => { it('renders the copyToClipboard button', () => {
expect(wrapper.findComponent(ModalCopyButton).exists()).toBe(true); expect(wrapper.findComponent(ModalCopyButton).exists()).toBe(true);
}); });
it('emits change events from the modal up', () => {
modal.vm.$emit('change', false);
expect(wrapper.emitted('change')).toEqual([[false]]);
});
it('passes visible to the modal', () => {
expect(modal.props('visible')).toBe(true);
});
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { mount } from '@vue/test-utils'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { s__ } from '~/locale';
import EnvironmentsApp from '~/environments/components/new_environments_app.vue'; import EnvironmentsApp from '~/environments/components/new_environments_app.vue';
import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue'; import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data'; import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
...@@ -22,7 +23,16 @@ describe('~/environments/components/new_environments_app.vue', () => { ...@@ -22,7 +23,16 @@ describe('~/environments/components/new_environments_app.vue', () => {
return createMockApollo([], mockResolvers); return createMockApollo([], mockResolvers);
}; };
const createWrapper = (apolloProvider) => mount(EnvironmentsApp, { apolloProvider }); const createWrapper = ({ provide = {}, apolloProvider } = {}) =>
mountExtended(EnvironmentsApp, {
provide: {
newEnvironmentPath: '/environments/new',
canCreateEnvironment: true,
defaultBranchName: 'main',
...provide,
},
apolloProvider,
});
beforeEach(() => { beforeEach(() => {
environmentAppMock = jest.fn(); environmentAppMock = jest.fn();
...@@ -37,7 +47,7 @@ describe('~/environments/components/new_environments_app.vue', () => { ...@@ -37,7 +47,7 @@ describe('~/environments/components/new_environments_app.vue', () => {
environmentAppMock.mockReturnValue(resolvedEnvironmentsApp); environmentAppMock.mockReturnValue(resolvedEnvironmentsApp);
environmentFolderMock.mockReturnValue(resolvedFolder); environmentFolderMock.mockReturnValue(resolvedFolder);
const apolloProvider = createApolloProvider(); const apolloProvider = createApolloProvider();
wrapper = createWrapper(apolloProvider); wrapper = createWrapper({ apolloProvider });
await waitForPromises(); await waitForPromises();
await Vue.nextTick(); await Vue.nextTick();
...@@ -47,4 +57,66 @@ describe('~/environments/components/new_environments_app.vue', () => { ...@@ -47,4 +57,66 @@ describe('~/environments/components/new_environments_app.vue', () => {
expect(text).toContainEqual(expect.stringMatching('review')); expect(text).toContainEqual(expect.stringMatching('review'));
expect(text).not.toContainEqual(expect.stringMatching('production')); expect(text).not.toContainEqual(expect.stringMatching('production'));
}); });
it('should show a button to create a new environment', async () => {
environmentAppMock.mockReturnValue(resolvedEnvironmentsApp);
environmentFolderMock.mockReturnValue(resolvedFolder);
const apolloProvider = createApolloProvider();
wrapper = createWrapper({ apolloProvider });
await waitForPromises();
await Vue.nextTick();
const button = wrapper.findByRole('link', { name: s__('Environments|New environment') });
expect(button.attributes('href')).toBe('/environments/new');
});
it('should not show a button to create a new environment if the user has no permissions', async () => {
environmentAppMock.mockReturnValue(resolvedEnvironmentsApp);
environmentFolderMock.mockReturnValue(resolvedFolder);
const apolloProvider = createApolloProvider();
wrapper = createWrapper({
apolloProvider,
provide: { canCreateEnvironment: false, newEnvironmentPath: '' },
});
await waitForPromises();
await Vue.nextTick();
const button = wrapper.findByRole('link', { name: s__('Environments|New environment') });
expect(button.exists()).toBe(false);
});
it('should show a button to open the review app modal', async () => {
environmentAppMock.mockReturnValue(resolvedEnvironmentsApp);
environmentFolderMock.mockReturnValue(resolvedFolder);
const apolloProvider = createApolloProvider();
wrapper = createWrapper({ apolloProvider });
await waitForPromises();
await Vue.nextTick();
const button = wrapper.findByRole('button', { name: s__('Environments|Enable review app') });
button.trigger('click');
await Vue.nextTick();
expect(wrapper.findByText(s__('ReviewApp|Enable Review App')).exists()).toBe(true);
});
it('should not show a button to open the review app modal if review apps are configured', async () => {
environmentAppMock.mockReturnValue({
...resolvedEnvironmentsApp,
reviewApp: { canSetupReviewApp: false },
});
environmentFolderMock.mockReturnValue(resolvedFolder);
const apolloProvider = createApolloProvider();
wrapper = createWrapper({ apolloProvider });
await waitForPromises();
await Vue.nextTick();
const button = wrapper.findByRole('button', { name: s__('Environments|Enable review app') });
expect(button.exists()).toBe(false);
});
}); });
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