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