Commit 2a7c3adf authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch 'nfriend-reorganize-release-detail-page-store' into 'master'

Convert individual release page to VueApollo [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!56882
parents 3a47bd09 13e11cdc
<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