Commit 6c63b0de authored by Filipa Lacerda's avatar Filipa Lacerda

Provides release as a single prop

Uses public api

Updates releases component to receive just one prop
Updates releases component to be able to not render assets
parent e7574ad7
...@@ -32,6 +32,7 @@ const Api = { ...@@ -32,6 +32,7 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches', createBranchPath: '/api/:version/projects/:id/repository/branches',
geoNodesPath: '/api/:version/geo_nodes', geoNodesPath: '/api/:version/geo_nodes',
subscriptionPath: '/api/:version/namespaces/:id/gitlab_subscription', subscriptionPath: '/api/:version/namespaces/:id/gitlab_subscription',
releasesPath: '/api/:version/project/:id/releases',
group(groupId, callback) { group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
...@@ -352,6 +353,12 @@ const Api = { ...@@ -352,6 +353,12 @@ const Api = {
return axios.get(url); return axios.get(url);
}, },
releases(id) {
const url = Api.buildUrl(this.releasesPath).replace(':id', encodeURIComponent(id));
return axios.get(url);
},
buildUrl(url) { buildUrl(url) {
let urlRoot = ''; let urlRoot = '';
if (gon.relative_url_root != null) { if (gon.relative_url_root != null) {
......
...@@ -11,7 +11,7 @@ export default { ...@@ -11,7 +11,7 @@ export default {
ReleaseBlock, ReleaseBlock,
}, },
props: { props: {
endpoint: { projectId: {
type: String, type: String,
required: true, required: true,
}, },
...@@ -27,28 +27,23 @@ export default { ...@@ -27,28 +27,23 @@ export default {
computed: { computed: {
...mapState(['isLoading', 'releases', 'hasError']), ...mapState(['isLoading', 'releases', 'hasError']),
shouldRenderEmptyState() { shouldRenderEmptyState() {
return !this.releases.length && !this.hasError; return !this.releases.length && !this.hasError && !this.isLoading;
}, },
shouldRenderSuccessState() { shouldRenderSuccessState() {
return this.releases.length && !this.isLoading && !this.hasError; return this.releases.length && !this.isLoading && !this.hasError;
}, },
}, },
created() { created() {
this.setEndpoint(this.endpoint); this.fetchReleases(this.projectId);
this.fetchReleases();
}, },
methods: { methods: {
...mapActions(['setEndpoint', 'fetchReleases']), ...mapActions(['fetchReleases']),
}, },
}; };
</script> </script>
<template> <template>
<div class="prepend-top-default"> <div class="prepend-top-default">
<gl-loading-icon <gl-loading-icon v-if="isLoading" :size="2" class="js-loading prepend-top-20" />
v-if="isLoading"
:size="2"
class="js-loading qa-loading-animation prepend-top-20"
/>
<gl-empty-state <gl-empty-state
v-else-if="shouldRenderEmptyState" v-else-if="shouldRenderEmptyState"
...@@ -68,15 +63,7 @@ export default { ...@@ -68,15 +63,7 @@ export default {
<release-block <release-block
v-for="(release, index) in releases" v-for="(release, index) in releases"
:key="release.tag_name" :key="release.tag_name"
:name="release.name" :release="release"
:tag="release.tag_name"
:commit="release.commit"
:description="release.description_html"
:author="release.commit.author"
:created-at="release.created_at"
:assets-count="release.assets.count"
:sources="release.assets.sources"
:links="release.assets.links"
:class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }" :class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
/> />
</div> </div>
......
...@@ -17,67 +17,36 @@ export default { ...@@ -17,67 +17,36 @@ export default {
}, },
mixins: [timeagoMixin], mixins: [timeagoMixin],
props: { props: {
name: { release: {
type: String,
required: true,
},
tag: {
type: String,
required: true,
},
commit: {
type: Object, type: Object,
required: true, required: true,
}, default: () => ({}),
description: {
type: String,
required: false,
default: '',
},
author: {
type: Object,
required: false,
default: null,
},
createdAt: {
type: String,
required: false,
default: '',
},
assetsCount: {
type: Number,
required: false,
default: 0,
},
sources: {
type: Array,
required: false,
default: () => [],
},
links: {
type: Array,
required: false,
default: () => [],
}, },
}, },
computed: { computed: {
releasedTimeAgo() { releasedTimeAgo() {
return sprintf('released %{time}', { return sprintf('released %{time}', {
time: this.timeFormated(this.createdAt), time: this.timeFormated(this.release.created_at),
}); });
}, },
userImageAltDescription() { userImageAltDescription() {
return this.author && this.author.username return this.commit.author && this.commit.author.username
? sprintf("%{username}'s avatar", { username: this.author.username }) ? sprintf("%{username}'s avatar", { username: this.commit.author.username })
: null; : null;
}, },
commit() {
return this.release.commit || {};
},
assets() {
return this.release.assets || {};
},
}, },
}; };
</script> </script>
<template> <template>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h2 class="card-title mt-0">{{ name }}</h2> <h2 class="card-title mt-0">{{ release.name }}</h2>
<div class="card-subtitle d-flex flex-wrap text-secondary"> <div class="card-subtitle d-flex flex-wrap text-secondary">
<div class="append-right-8"> <div class="append-right-8">
...@@ -87,33 +56,39 @@ export default { ...@@ -87,33 +56,39 @@ export default {
<div class="append-right-8"> <div class="append-right-8">
<icon name="tag" class="align-middle" /> <icon name="tag" class="align-middle" />
<span v-gl-tooltip.bottom :title="__('Tag')">{{ tag }}</span> <span v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span>
</div> </div>
<div class="append-right-4"> <div class="append-right-4">
&bull; &bull;
<span v-gl-tooltip.bottom :title="tooltipTitle(createdAt)">{{ releasedTimeAgo }}</span> <span v-gl-tooltip.bottom :title="tooltipTitle(release.created_at)">{{
releasedTimeAgo
}}</span>
</div> </div>
<div v-if="author" class="d-flex"> <div v-if="commit.author" class="d-flex">
by by
<user-avatar-link <user-avatar-link
class="prepend-left-4" class="prepend-left-4"
:link-href="author.path" :link-href="commit.author.path"
:img-src="author.avatar_url" :img-src="commit.author.avatar_url"
:img-alt="userImageAltDescription" :img-alt="userImageAltDescription"
:tooltip-text="author.username" :tooltip-text="commit.author.username"
/> />
</div> </div>
</div> </div>
<div class="card-text prepend-top-default"> <div
v-if="assets.links.length || assets.sources.length"
Sclass="card-text prepend-top-default"
>
<b> <b>
{{ __('Assets') }} <span class="js-assets-count badge badge-pill">{{ assetsCount }}</span> {{ __('Assets') }}
<span class="js-assets-count badge badge-pill">{{ assets.count }}</span>
</b> </b>
<ul class="pl-0 mb-0 prepend-top-8 list-unstyled js-assets-list"> <ul v-if="assets.links.length" class="pl-0 mb-0 prepend-top-8 list-unstyled js-assets-list">
<li v-for="link in links" :key="link.name" class="append-bottom-8"> <li v-for="link in assets.links" :key="link.name" class="append-bottom-8">
<gl-link v-gl-tooltip.bottom :title="__('Download asset')" :href="link.url"> <gl-link v-gl-tooltip.bottom :title="__('Download asset')" :href="link.url">
<icon name="package" class="align-middle append-right-4 align-text-bottom" /> <icon name="package" class="align-middle append-right-4 align-text-bottom" />
{{ link.name }} {{ link.name }}
...@@ -121,7 +96,7 @@ export default { ...@@ -121,7 +96,7 @@ export default {
</li> </li>
</ul> </ul>
<div class="dropdown"> <div v-if="assets.sources.length" class="dropdown">
<button <button
type="button" type="button"
class="btn btn-link" class="btn btn-link"
...@@ -134,14 +109,14 @@ export default { ...@@ -134,14 +109,14 @@ export default {
</button> </button>
<div class="js-sources-dropdown dropdown-menu"> <div class="js-sources-dropdown dropdown-menu">
<li v-for="asset in sources" :key="asset.url"> <li v-for="asset in assets.sources" :key="asset.url">
<gl-link :href="asset.url">{{ __('Download') }} {{ asset.format }}</gl-link> <gl-link :href="asset.url">{{ __('Download') }} {{ asset.format }}</gl-link>
</li> </li>
</div> </div>
</div> </div>
</div> </div>
<div class="card-text prepend-top-default"><div v-html="description"></div></div> <div class="card-text prepend-top-default"><div v-html="release.description_html"></div></div>
</div> </div>
</div> </div>
</template> </template>
import * as types from './mutation_types'; import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import api from '~/api';
/**
* Commits a mutation to store the main endpoint.
*
* @param {String} endpoint
*/
export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
/** /**
* Commits a mutation to update the state while the main endpoint is being requested. * Commits a mutation to update the state while the main endpoint is being requested.
...@@ -20,14 +13,19 @@ export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES); ...@@ -20,14 +13,19 @@ export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES);
* Will dispatch requestNamespace action before starting the request. * Will dispatch requestNamespace action before starting the request.
* Will dispatch receiveNamespaceSuccess if the request is successfull * Will dispatch receiveNamespaceSuccess if the request is successfull
* Will dispatch receiveNamesapceError if the request returns an error * Will dispatch receiveNamesapceError if the request returns an error
*
* @param {String} projectId
*/ */
export const fetchReleases = ({ state, dispatch }) => { export const fetchReleases = ({ dispatch }, projectId) => {
dispatch('requestReleases'); dispatch('requestReleases');
axios api
.get(state.endpoint) .releases(projectId)
.then(({ data }) => dispatch('receiveReleasesSuccess', data)) .then(({ data }) => dispatch('receiveReleasesSuccess', data))
.catch(() => dispatch('receiveReleasesError')); .catch(error => {
console.log(error);
dispatch('receiveReleasesError');
});
}; };
export const receiveReleasesSuccess = ({ commit }, data) => export const receiveReleasesSuccess = ({ commit }, data) =>
......
export const SET_ENDPOINT = 'SET_ENDPOINT';
export const REQUEST_RELEASES = 'REQUEST_RELEASES'; export const REQUEST_RELEASES = 'REQUEST_RELEASES';
export const RECEIVE_RELEASES_SUCCESS = 'RECEIVE_RELEASES_SUCCESS'; export const RECEIVE_RELEASES_SUCCESS = 'RECEIVE_RELEASES_SUCCESS';
export const RECEIVE_RELEASES_ERROR = 'RECEIVE_RELEASES_ERROR'; export const RECEIVE_RELEASES_ERROR = 'RECEIVE_RELEASES_ERROR';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
/**
* Sets the main endpoint
* @param {Object} state
* @param {String} endpoint
*/
[types.SET_ENDPOINT](state, endpoint) {
state.endpoint = endpoint;
},
/** /**
* Sets isLoading to true while the request is being made. * Sets isLoading to true while the request is being made.
* @param {Object} state * @param {Object} state
......
export default () => ({ export default () => ({
endpoint: null,
isLoading: false, isLoading: false,
hasError: false, hasError: false,
releases: [], releases: [],
......
- @no_container = true - @no_container = true
- page_title _('Releases') - page_title _('Releases')
%div{ 'class' => container_class } %div{ class: container_class }
#js-releases-page{ data: { endpoint: project_releases_path(@project, format: :json), illustration_path: image_path('illustrations/releases.svg'), documentation_path: help_page_path('user/releases') } } #js-releases-page{ data: { project_id: @project.id, illustration_path: image_path('illustrations/releases.svg'), documentation_path: help_page_path('user/releases') } }
...@@ -5,4 +5,4 @@ ...@@ -5,4 +5,4 @@
A list of the published Releases are available to everyone. A list of the published Releases are available to everyone.
Nagivate to Projects > Releases in order to see the list of releases of a project: Nagivate to Projects > Releases in order to see the list of releases of a project:
![Releases List](img/releases.png) ![Releases List](img/releases.png)
\ No newline at end of file
import Vue from 'vue'; import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import app from '~/releases/components/app.vue'; import app from '~/releases/components/app.vue';
import createStore from '~/releases/store'; import createStore from '~/releases/store';
import api from '~/api';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../store/helpers'; import { resetStore } from '../store/helpers';
import { releases } from '../mock_data'; import { releases } from '../mock_data';
...@@ -11,28 +10,25 @@ describe('Releases App ', () => { ...@@ -11,28 +10,25 @@ describe('Releases App ', () => {
const Component = Vue.extend(app); const Component = Vue.extend(app);
let store; let store;
let vm; let vm;
let mock;
const props = { const props = {
endpoint: 'endpoint.json', projectId: 'gitlab-ce',
documentationLink: 'help/releases', documentationLink: 'help/releases',
illustrationPath: 'illustration/path', illustrationPath: 'illustration/path',
}; };
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios);
store = createStore(); store = createStore();
}); });
afterEach(() => { afterEach(() => {
resetStore(store); resetStore(store);
vm.$destroy(); vm.$destroy();
mock.restore();
}); });
describe('while loading', () => { describe('while loading', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(props.endpoint).replyOnce(200, [], {}); spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [] }));
vm = mountComponentWithStore(Component, { props, store }); vm = mountComponentWithStore(Component, { props, store });
}); });
...@@ -49,7 +45,7 @@ describe('Releases App ', () => { ...@@ -49,7 +45,7 @@ describe('Releases App ', () => {
describe('with successful request', () => { describe('with successful request', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(props.endpoint).reply(200, releases); spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: releases }));
vm = mountComponentWithStore(Component, { props, store }); vm = mountComponentWithStore(Component, { props, store });
}); });
...@@ -66,7 +62,7 @@ describe('Releases App ', () => { ...@@ -66,7 +62,7 @@ describe('Releases App ', () => {
describe('with empty request', () => { describe('with empty request', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(props.endpoint).reply(200, []); spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [] }));
vm = mountComponentWithStore(Component, { props, store }); vm = mountComponentWithStore(Component, { props, store });
}); });
......
...@@ -28,6 +28,16 @@ describe('Release block', () => { ...@@ -28,6 +28,16 @@ describe('Release block', () => {
committer_name: 'Jack Smith', committer_name: 'Jack Smith',
committer_email: 'jack@example.com', committer_email: 'jack@example.com',
committed_date: '2012-05-28T04:42:42-07:00', committed_date: '2012-05-28T04:42:42-07:00',
author: {
avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
id: 482476,
name: 'John Doe',
path: '/johndoe',
state: 'active',
status_tooltip_html: null,
username: 'johndoe',
web_url: 'https://gitlab.com/johndoe',
},
}, },
assets: { assets: {
count: 6, count: 6,
...@@ -66,32 +76,10 @@ describe('Release block', () => { ...@@ -66,32 +76,10 @@ describe('Release block', () => {
], ],
}, },
}; };
const props = {
name: release.name,
tag: release.tag_name,
commit: release.commit,
description: release.description_html,
author: {
avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
id: 482476,
name: 'John Doe',
path: '/johndoe',
state: 'active',
status_tooltip_html: null,
username: 'johndoe',
web_url: 'https://gitlab.com/johndoe',
},
createdAt: release.created_at,
assetsCount: release.assets.count,
sources: release.assets.sources,
links: release.assets.links,
};
let vm; let vm;
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, props); vm = mountComponent(Component, { release });
}); });
afterEach(() => { afterEach(() => {
......
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { import {
setEndpoint,
requestReleases, requestReleases,
fetchReleases, fetchReleases,
receiveReleasesSuccess, receiveReleasesSuccess,
...@@ -9,6 +6,7 @@ import { ...@@ -9,6 +6,7 @@ import {
} from '~/releases/store/actions'; } from '~/releases/store/actions';
import state from '~/releases/store/state'; import state from '~/releases/store/state';
import * as types from '~/releases/store/mutation_types'; import * as types from '~/releases/store/mutation_types';
import api from '~/api';
import testAction from 'spec/helpers/vuex_action_helper'; import testAction from 'spec/helpers/vuex_action_helper';
import { releases } from '../mock_data'; import { releases } from '../mock_data';
...@@ -19,19 +17,6 @@ describe('Releases State actions', () => { ...@@ -19,19 +17,6 @@ describe('Releases State actions', () => {
mockedState = state(); mockedState = state();
}); });
describe('setEndpoint', () => {
it('should commit SET_ENDPOINT mutation', done => {
testAction(
setEndpoint,
'endpoint.json',
mockedState,
[{ type: types.SET_ENDPOINT, payload: 'endpoint.json' }],
[],
done,
);
});
});
describe('requestReleases', () => { describe('requestReleases', () => {
it('should commit REQUEST_RELEASES mutation', done => { it('should commit REQUEST_RELEASES mutation', done => {
testAction(requestReleases, null, mockedState, [{ type: types.REQUEST_RELEASES }], [], done); testAction(requestReleases, null, mockedState, [{ type: types.REQUEST_RELEASES }], [], done);
...@@ -39,20 +24,9 @@ describe('Releases State actions', () => { ...@@ -39,20 +24,9 @@ describe('Releases State actions', () => {
}); });
describe('fetchReleases', () => { describe('fetchReleases', () => {
let mock;
beforeEach(() => {
mockedState.endpoint = 'endpoint.json';
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('success', () => { describe('success', () => {
it('dispatches requestReleases and receiveReleasesSuccess ', done => { it('dispatches requestReleases and receiveReleasesSuccess ', done => {
mock.onGet('endpoint.json').replyOnce(200, releases); spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: releases }));
testAction( testAction(
fetchReleases, fetchReleases,
...@@ -74,11 +48,9 @@ describe('Releases State actions', () => { ...@@ -74,11 +48,9 @@ describe('Releases State actions', () => {
}); });
describe('error', () => { describe('error', () => {
beforeEach(() => {
mock.onGet('endpoint.json').replyOnce(500);
});
it('dispatches requestReleases and receiveReleasesError ', done => { it('dispatches requestReleases and receiveReleasesError ', done => {
spyOn(api, 'releases').and.returnValue(Promise.reject());
testAction( testAction(
fetchReleases, fetchReleases,
null, null,
......
...@@ -10,14 +10,6 @@ describe('Releases Store Mutations', () => { ...@@ -10,14 +10,6 @@ describe('Releases Store Mutations', () => {
stateCopy = state(); stateCopy = state();
}); });
describe('SET_ENDPOINT', () => {
it('should set endpoint', () => {
mutations[types.SET_ENDPOINT](stateCopy, 'endpoint.json');
expect(stateCopy.endpoint).toEqual('endpoint.json');
});
});
describe('REQUEST_RELEASES', () => { describe('REQUEST_RELEASES', () => {
it('sets isLoading to true', () => { it('sets isLoading to true', () => {
mutations[types.REQUEST_RELEASES](stateCopy); mutations[types.REQUEST_RELEASES](stateCopy);
......
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