Commit 13e11cdc authored by Nathan Friend's avatar Nathan Friend Committed by Enrique Alcántara

Convert individual release page to VueApollo

This commit updates the "individual" (AKA "show") release page to use
VueApollo to fetch data instead of a Vuex store.
parent e10f7ccd
<script> <script>
import { mapState, mapActions } from 'vuex'; import createFlash from '~/flash';
import { s__ } from '~/locale';
import oneReleaseQuery from '../queries/one_release.query.graphql';
import { convertGraphQLRelease } from '../util';
import ReleaseBlock from './release_block.vue'; import ReleaseBlock from './release_block.vue';
import ReleaseSkeletonLoader from './release_skeleton_loader.vue'; import ReleaseSkeletonLoader from './release_skeleton_loader.vue';
...@@ -9,21 +12,58 @@ export default { ...@@ -9,21 +12,58 @@ export default {
ReleaseBlock, ReleaseBlock,
ReleaseSkeletonLoader, ReleaseSkeletonLoader,
}, },
computed: { inject: {
...mapState('detail', ['isFetchingRelease', 'fetchError', 'release']), fullPath: {
default: '',
},
tagName: {
default: '',
},
}, },
created() { apollo: {
this.fetchRelease(); release: {
query: oneReleaseQuery,
variables() {
return {
fullPath: this.fullPath,
tagName: this.tagName,
};
},
update(data) {
if (data.project?.release) {
return convertGraphQLRelease(data.project.release);
}
return null;
},
result(result) {
// Handle the case where the query succeeded but didn't return any data
if (!result.error && !this.release) {
this.showFlash(
new Error(`No release found in project "${this.fullPath}" with tag "${this.tagName}"`),
);
}
},
error(error) {
this.showFlash(error);
},
},
}, },
methods: { methods: {
...mapActions('detail', ['fetchRelease']), showFlash(error) {
createFlash({
message: s__('Release|Something went wrong while getting the release details.'),
captureError: true,
error,
});
},
}, },
}; };
</script> </script>
<template> <template>
<div class="gl-mt-3"> <div class="gl-mt-3">
<release-skeleton-loader v-if="isFetchingRelease" /> <release-skeleton-loader v-if="$apollo.queries.release.loading" />
<release-block v-else-if="!fetchError" :release="release" /> <release-block v-else-if="release" :release="release" />
</div> </div>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import ReleaseShowApp from './components/app_show.vue'; import ReleaseShowApp from './components/app_show.vue';
import createStore from './stores';
import createDetailModule from './stores/modules/detail';
Vue.use(Vuex); Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => { export default () => {
const el = document.getElementById('js-show-release-page'); const el = document.getElementById('js-show-release-page');
const store = createStore({ if (!el) return false;
modules: {
detail: createDetailModule(el.dataset), const { projectPath, tagName } = el.dataset;
},
featureFlags: {
graphqlIndividualReleasePage: Boolean(gon.features?.graphqlIndividualReleasePage),
},
});
return new Vue({ return new Vue({
el, el,
store, apolloProvider,
provide: {
fullPath: projectPath,
tagName,
},
render: (h) => h(ReleaseShowApp), render: (h) => h(ReleaseShowApp),
}); });
}; };
...@@ -43,7 +43,7 @@ export const fetchRelease = ({ commit, state, rootState }) => { ...@@ -43,7 +43,7 @@ export const fetchRelease = ({ commit, state, rootState }) => {
}) })
.catch((error) => { .catch((error) => {
commit(types.RECEIVE_RELEASE_ERROR, error); commit(types.RECEIVE_RELEASE_ERROR, error);
createFlash(s__('Release|Something went wrong while getting the release details')); createFlash(s__('Release|Something went wrong while getting the release details.'));
}); });
} }
...@@ -54,7 +54,7 @@ export const fetchRelease = ({ commit, state, rootState }) => { ...@@ -54,7 +54,7 @@ export const fetchRelease = ({ commit, state, rootState }) => {
}) })
.catch((error) => { .catch((error) => {
commit(types.RECEIVE_RELEASE_ERROR, error); commit(types.RECEIVE_RELEASE_ERROR, error);
createFlash(s__('Release|Something went wrong while getting the release details')); createFlash(s__('Release|Something went wrong while getting the release details.'));
}); });
}; };
......
...@@ -12,7 +12,6 @@ class Projects::ReleasesController < Projects::ApplicationController ...@@ -12,7 +12,6 @@ class Projects::ReleasesController < Projects::ApplicationController
push_frontend_feature_flag(:graphql_release_data, project, default_enabled: true) push_frontend_feature_flag(:graphql_release_data, project, default_enabled: true)
push_frontend_feature_flag(:graphql_milestone_stats, project, default_enabled: true) push_frontend_feature_flag(:graphql_milestone_stats, project, default_enabled: true)
push_frontend_feature_flag(:graphql_releases_page, project, default_enabled: true) push_frontend_feature_flag(:graphql_releases_page, project, default_enabled: true)
push_frontend_feature_flag(:graphql_individual_release_page, project, default_enabled: true)
end end
before_action :authorize_update_release!, only: %i[edit update] before_action :authorize_update_release!, only: %i[edit update]
before_action :authorize_create_release!, only: :new before_action :authorize_create_release!, only: :new
......
---
title: Remove graphql_individual_release_page feature flag
merge_request: 56882
author:
type: removed
---
name: graphql_individual_release_page
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44779
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263522
milestone: '13.5'
type: development
group: group::release
default_enabled: true
...@@ -25348,7 +25348,7 @@ msgstr "" ...@@ -25348,7 +25348,7 @@ msgstr ""
msgid "Release|Something went wrong while creating a new release" msgid "Release|Something went wrong while creating a new release"
msgstr "" msgstr ""
msgid "Release|Something went wrong while getting the release details" msgid "Release|Something went wrong while getting the release details."
msgstr "" msgstr ""
msgid "Release|Something went wrong while saving the release details" msgid "Release|Something went wrong while saving the release details"
......
...@@ -5,7 +5,6 @@ require 'spec_helper' ...@@ -5,7 +5,6 @@ require 'spec_helper'
RSpec.describe 'User views Release', :js do RSpec.describe 'User views Release', :js do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:graphql_feature_flag) { true }
let(:release) do let(:release) do
create(:release, create(:release,
...@@ -15,8 +14,6 @@ RSpec.describe 'User views Release', :js do ...@@ -15,8 +14,6 @@ RSpec.describe 'User views Release', :js do
end end
before do before do
stub_feature_flags(graphql_individual_release_page: graphql_feature_flag)
project.add_developer(user) project.add_developer(user)
sign_in(user) sign_in(user)
...@@ -26,35 +23,23 @@ RSpec.describe 'User views Release', :js do ...@@ -26,35 +23,23 @@ RSpec.describe 'User views Release', :js do
it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet' it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
shared_examples 'release page' do it 'renders the breadcrumbs' do
it 'renders the breadcrumbs' do within('.breadcrumbs') do
within('.breadcrumbs') do expect(page).to have_content("#{project.creator.name} #{project.name} Releases #{release.name}")
expect(page).to have_content("#{project.creator.name} #{project.name} Releases #{release.name}")
expect(page).to have_link(project.creator.name, href: user_path(project.creator))
expect(page).to have_link(project.name, href: project_path(project))
expect(page).to have_link('Releases', href: project_releases_path(project))
expect(page).to have_link(release.name, href: project_release_path(project, release))
end
end
it 'renders the release details' do expect(page).to have_link(project.creator.name, href: user_path(project.creator))
within('.release-block') do expect(page).to have_link(project.name, href: project_path(project))
expect(page).to have_content(release.name) expect(page).to have_link('Releases', href: project_releases_path(project))
expect(page).to have_content(release.tag) expect(page).to have_link(release.name, href: project_release_path(project, release))
expect(page).to have_content(release.commit.short_id)
expect(page).to have_content('Lorem ipsum dolor sit amet')
end
end end
end end
describe 'when the graphql_individual_release_page feature flag is enabled' do it 'renders the release details' do
it_behaves_like 'release page' within('.release-block') do
end expect(page).to have_content(release.name)
expect(page).to have_content(release.tag)
describe 'when the graphql_individual_release_page feature flag is disabled' do expect(page).to have_content(release.commit.short_id)
let(:graphql_feature_flag) { false } expect(page).to have_content('Lorem ipsum dolor sit amet')
end
it_behaves_like 'release page'
end end
end end
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { getJSONFixture } from 'helpers/fixtures'; import { getJSONFixture } from 'helpers/fixtures';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import createMockApollo from 'helpers/mock_apollo_helper';
import createFlash from '~/flash';
import ReleaseShowApp from '~/releases/components/app_show.vue'; import ReleaseShowApp from '~/releases/components/app_show.vue';
import ReleaseBlock from '~/releases/components/release_block.vue'; import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue'; import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
import oneReleaseQuery from '~/releases/queries/one_release.query.graphql';
const originalRelease = getJSONFixture('api/releases/release.json'); jest.mock('~/flash');
const oneReleaseQueryResponse = getJSONFixture(
'graphql/releases/queries/one_release.query.graphql.json',
);
Vue.use(VueApollo);
const EXPECTED_ERROR_MESSAGE = 'Something went wrong while getting the release details.';
const MOCK_FULL_PATH = 'project/full/path';
const MOCK_TAG_NAME = 'test-tag-name';
describe('Release show component', () => { describe('Release show component', () => {
let wrapper; let wrapper;
let release;
let actions;
beforeEach(() => { const createComponent = ({ apolloProvider }) => {
release = convertObjectPropsToCamelCase(originalRelease); wrapper = shallowMount(ReleaseShowApp, {
}); provide: {
fullPath: MOCK_FULL_PATH,
const factory = (state) => { tagName: MOCK_TAG_NAME,
actions = {
fetchRelease: jest.fn(),
};
const store = new Vuex.Store({
modules: {
detail: {
namespaced: true,
actions,
state,
},
}, },
apolloProvider,
}); });
wrapper = shallowMount(ReleaseShowApp, { store });
}; };
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findLoadingSkeleton = () => wrapper.find(ReleaseSkeletonLoader); const findLoadingSkeleton = () => wrapper.find(ReleaseSkeletonLoader);
const findReleaseBlock = () => wrapper.find(ReleaseBlock); const findReleaseBlock = () => wrapper.find(ReleaseBlock);
it('calls fetchRelease when the component is created', () => { const expectLoadingIndicator = () => {
factory({ release }); it('renders a loading indicator', () => {
expect(actions.fetchRelease).toHaveBeenCalledTimes(1); expect(findLoadingSkeleton().exists()).toBe(true);
});
};
const expectNoLoadingIndicator = () => {
it('does not render a loading indicator', () => {
expect(findLoadingSkeleton().exists()).toBe(false);
});
};
const expectNoFlash = () => {
it('does not show a flash message', () => {
expect(createFlash).not.toHaveBeenCalled();
});
};
const expectFlashWithMessage = (message) => {
it(`shows a flash message that reads "${message}"`, () => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith({
message,
captureError: true,
error: expect.any(Error),
});
});
};
const expectReleaseBlock = () => {
it('renders a release block', () => {
expect(findReleaseBlock().exists()).toBe(true);
});
};
const expectNoReleaseBlock = () => {
it('does not render a release block', () => {
expect(findReleaseBlock().exists()).toBe(false);
});
};
describe('GraphQL query variables', () => {
const queryHandler = jest.fn().mockResolvedValueOnce(oneReleaseQueryResponse);
beforeEach(() => {
const apolloProvider = createMockApollo([[oneReleaseQuery, queryHandler]]);
createComponent({ apolloProvider });
});
it('builds a GraphQL with the expected variables', () => {
expect(queryHandler).toHaveBeenCalledTimes(1);
expect(queryHandler).toHaveBeenCalledWith({
fullPath: MOCK_FULL_PATH,
tagName: MOCK_TAG_NAME,
});
});
}); });
it('shows a loading skeleton and hides the release block while the API call is in progress', () => { describe('when the component is loading data', () => {
factory({ isFetchingRelease: true }); beforeEach(() => {
expect(findLoadingSkeleton().exists()).toBe(true); const apolloProvider = createMockApollo([
expect(findReleaseBlock().exists()).toBe(false); [oneReleaseQuery, jest.fn().mockReturnValueOnce(new Promise(() => {}))],
]);
createComponent({ apolloProvider });
});
expectLoadingIndicator();
expectNoFlash();
expectNoReleaseBlock();
}); });
it('hides the loading skeleton and shows the release block when the API call finishes successfully', () => { describe('when the component has successfully loaded the release', () => {
factory({ isFetchingRelease: false }); beforeEach(() => {
expect(findLoadingSkeleton().exists()).toBe(false); const apolloProvider = createMockApollo([
expect(findReleaseBlock().exists()).toBe(true); [oneReleaseQuery, jest.fn().mockResolvedValueOnce(oneReleaseQueryResponse)],
]);
createComponent({ apolloProvider });
});
expectNoLoadingIndicator();
expectNoFlash();
expectReleaseBlock();
}); });
it('hides both the loading skeleton and the release block when the API call fails', () => { describe('when the request succeeded, but the returned "project" key was null', () => {
factory({ fetchError: new Error('Uh oh') }); beforeEach(() => {
expect(findLoadingSkeleton().exists()).toBe(false); const apolloProvider = createMockApollo([
expect(findReleaseBlock().exists()).toBe(false); [oneReleaseQuery, jest.fn().mockResolvedValueOnce({ data: { project: null } })],
]);
createComponent({ apolloProvider });
});
expectNoLoadingIndicator();
expectFlashWithMessage(EXPECTED_ERROR_MESSAGE);
expectNoReleaseBlock();
});
describe('when the request succeeded, but the returned "project.release" key was null', () => {
beforeEach(() => {
const apolloProvider = createMockApollo([
[
oneReleaseQuery,
jest.fn().mockResolvedValueOnce({ data: { project: { release: null } } }),
],
]);
createComponent({ apolloProvider });
});
expectNoLoadingIndicator();
expectFlashWithMessage(EXPECTED_ERROR_MESSAGE);
expectNoReleaseBlock();
});
describe('when an error occurs while loading the release', () => {
beforeEach(() => {
const apolloProvider = createMockApollo([
[oneReleaseQuery, jest.fn().mockRejectedValueOnce('An error occurred!')],
]);
createComponent({ apolloProvider });
});
expectNoLoadingIndicator();
expectFlashWithMessage(EXPECTED_ERROR_MESSAGE);
expectNoReleaseBlock();
}); });
}); });
...@@ -163,7 +163,7 @@ describe('Release detail actions', () => { ...@@ -163,7 +163,7 @@ describe('Release detail actions', () => {
return actions.fetchRelease({ commit: jest.fn(), state, rootState: state }).then(() => { return actions.fetchRelease({ commit: jest.fn(), state, rootState: state }).then(() => {
expect(createFlash).toHaveBeenCalledTimes(1); expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith( expect(createFlash).toHaveBeenCalledWith(
'Something went wrong while getting the release details', 'Something went wrong while getting the release details.',
); );
}); });
}); });
......
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