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>
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 ReleaseSkeletonLoader from './release_skeleton_loader.vue';
......@@ -9,21 +12,58 @@ export default {
ReleaseBlock,
ReleaseSkeletonLoader,
},
computed: {
...mapState('detail', ['isFetchingRelease', 'fetchError', 'release']),
inject: {
fullPath: {
default: '',
},
tagName: {
default: '',
},
},
created() {
this.fetchRelease();
apollo: {
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: {
...mapActions('detail', ['fetchRelease']),
showFlash(error) {
createFlash({
message: s__('Release|Something went wrong while getting the release details.'),
captureError: true,
error,
});
},
},
};
</script>
<template>
<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>
</template>
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 createStore from './stores';
import createDetailModule from './stores/modules/detail';
Vue.use(Vuex);
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => {
const el = document.getElementById('js-show-release-page');
const store = createStore({
modules: {
detail: createDetailModule(el.dataset),
},
featureFlags: {
graphqlIndividualReleasePage: Boolean(gon.features?.graphqlIndividualReleasePage),
},
});
if (!el) return false;
const { projectPath, tagName } = el.dataset;
return new Vue({
el,
store,
apolloProvider,
provide: {
fullPath: projectPath,
tagName,
},
render: (h) => h(ReleaseShowApp),
});
};
......@@ -43,7 +43,7 @@ export const fetchRelease = ({ commit, state, rootState }) => {
})
.catch((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 }) => {
})
.catch((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
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_releases_page, project, default_enabled: true)
push_frontend_feature_flag(:graphql_individual_release_page, project, default_enabled: true)
end
before_action :authorize_update_release!, only: %i[edit update]
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 ""
msgid "Release|Something went wrong while creating a new release"
msgstr ""
msgid "Release|Something went wrong while getting the release details"
msgid "Release|Something went wrong while getting the release details."
msgstr ""
msgid "Release|Something went wrong while saving the release details"
......
......@@ -5,7 +5,6 @@ require 'spec_helper'
RSpec.describe 'User views Release', :js do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:graphql_feature_flag) { true }
let(:release) do
create(:release,
......@@ -15,8 +14,6 @@ RSpec.describe 'User views Release', :js do
end
before do
stub_feature_flags(graphql_individual_release_page: graphql_feature_flag)
project.add_developer(user)
sign_in(user)
......@@ -26,35 +23,23 @@ RSpec.describe 'User views Release', :js do
it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
shared_examples 'release page' do
it 'renders the breadcrumbs' do
within('.breadcrumbs') do
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 breadcrumbs' do
within('.breadcrumbs') do
expect(page).to have_content("#{project.creator.name} #{project.name} Releases #{release.name}")
it 'renders the release details' do
within('.release-block') do
expect(page).to have_content(release.name)
expect(page).to have_content(release.tag)
expect(page).to have_content(release.commit.short_id)
expect(page).to have_content('Lorem ipsum dolor sit amet')
end
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
describe 'when the graphql_individual_release_page feature flag is enabled' do
it_behaves_like 'release page'
end
describe 'when the graphql_individual_release_page feature flag is disabled' do
let(:graphql_feature_flag) { false }
it_behaves_like 'release page'
it 'renders the release details' do
within('.release-block') do
expect(page).to have_content(release.name)
expect(page).to have_content(release.tag)
expect(page).to have_content(release.commit.short_id)
expect(page).to have_content('Lorem ipsum dolor sit amet')
end
end
end
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 { 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 ReleaseBlock from '~/releases/components/release_block.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', () => {
let wrapper;
let release;
let actions;
beforeEach(() => {
release = convertObjectPropsToCamelCase(originalRelease);
});
const factory = (state) => {
actions = {
fetchRelease: jest.fn(),
};
const store = new Vuex.Store({
modules: {
detail: {
namespaced: true,
actions,
state,
},
const createComponent = ({ apolloProvider }) => {
wrapper = shallowMount(ReleaseShowApp, {
provide: {
fullPath: MOCK_FULL_PATH,
tagName: MOCK_TAG_NAME,
},
apolloProvider,
});
wrapper = shallowMount(ReleaseShowApp, { store });
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findLoadingSkeleton = () => wrapper.find(ReleaseSkeletonLoader);
const findReleaseBlock = () => wrapper.find(ReleaseBlock);
it('calls fetchRelease when the component is created', () => {
factory({ release });
expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
const expectLoadingIndicator = () => {
it('renders a loading indicator', () => {
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', () => {
factory({ isFetchingRelease: true });
expect(findLoadingSkeleton().exists()).toBe(true);
expect(findReleaseBlock().exists()).toBe(false);
describe('when the component is loading data', () => {
beforeEach(() => {
const apolloProvider = createMockApollo([
[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', () => {
factory({ isFetchingRelease: false });
expect(findLoadingSkeleton().exists()).toBe(false);
expect(findReleaseBlock().exists()).toBe(true);
describe('when the component has successfully loaded the release', () => {
beforeEach(() => {
const apolloProvider = createMockApollo([
[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', () => {
factory({ fetchError: new Error('Uh oh') });
expect(findLoadingSkeleton().exists()).toBe(false);
expect(findReleaseBlock().exists()).toBe(false);
describe('when the request succeeded, but the returned "project" key was null', () => {
beforeEach(() => {
const apolloProvider = createMockApollo([
[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', () => {
return actions.fetchRelease({ commit: jest.fn(), state, rootState: state }).then(() => {
expect(createFlash).toHaveBeenCalledTimes(1);
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