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> <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