Commit caab23bc authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '33905-connect-app-and-vuex' into 'master'

Finalise vue packages list app

See merge request gitlab-org/gitlab!20160
parents 4b6941e0 045e7585
......@@ -15,6 +15,7 @@ export default {
'/:project_full_path/environments/:environment_id/pods/:pod_name/containers/logs.json',
podLogsPathWithPodContainer:
'/:project_full_path/environments/:environment_id/pods/:pod_name/containers/:container_name/logs.json',
groupPackagesPath: '/api/:version/groups/:id/packages',
projectPackagesPath: '/api/:version/projects/:id/packages',
projectPackagePath: '/api/:version/projects/:id/packages/:package_id',
......@@ -109,9 +110,14 @@ export default {
return axios.get(url);
},
projectPackages(id) {
groupPackages(id, options = {}) {
const url = Api.buildUrl(this.groupPackagesPath).replace(':id', id);
return axios.get(url, options);
},
projectPackages(id, options = {}) {
const url = Api.buildUrl(this.projectPackagesPath).replace(':id', id);
return axios.get(url);
return axios.get(url, options);
},
buildProjectPackageUrl(projectId, packageId) {
......
<script>
import { mapState } from 'vuex';
import { GlTable, GlPagination, GlButton, GlSorting, GlSortingItem, GlModal } from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { s__, sprintf } from '~/locale';
import {
LIST_KEY_NAME,
LIST_KEY_PROJECT,
LIST_KEY_VERSION,
LIST_KEY_PACKAGE_TYPE,
LIST_KEY_CREATED_AT,
LIST_KEY_ACTIONS,
LIST_LABEL_NAME,
LIST_LABEL_PROJECT,
LIST_LABEL_VERSION,
LIST_LABEL_PACKAGE_TYPE,
LIST_LABEL_CREATED_AT,
LIST_LABEL_ACTIONS,
} from '../constants';
export default {
components: {
......@@ -15,13 +30,6 @@ export default {
GlModal,
Icon,
},
props: {
canDestroyPackage: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
modalId: 'confirm-delete-pacakge',
......@@ -29,22 +37,20 @@ export default {
};
},
computed: {
// the following computed properties are going to be connected to vuex
list() {
return [];
},
perPage() {
return 20;
},
totalItems() {
return 100;
},
...mapState({
list: 'packages',
perPage: state => state.pagination.perPage,
totalItems: state => state.pagination.total,
page: state => state.pagination.page,
canDestroyPackage: state => state.config.canDestroyPackage,
isGroupPage: state => state.config.isGroupPage,
}),
currentPage: {
get() {
return 1;
return this.page;
},
set() {
// do something with value
set(value) {
this.$emit('page:changed', value);
},
},
orderBy() {
......@@ -68,36 +74,45 @@ export default {
return this.canDestroyPackage;
},
sortableFields() {
// This list is filtered in the case of the project page, and the project column is removed
return [
{
key: 'name',
label: s__('Name'),
tdClass: ['w-25'],
key: LIST_KEY_NAME,
label: LIST_LABEL_NAME,
class: ['text-left'],
},
{
key: LIST_KEY_PROJECT,
label: LIST_LABEL_PROJECT,
class: ['text-center'],
},
{
key: 'version',
label: s__('Version'),
key: LIST_KEY_VERSION,
label: LIST_LABEL_VERSION,
class: ['text-center'],
},
{
key: 'package_type',
label: s__('Type'),
key: LIST_KEY_PACKAGE_TYPE,
label: LIST_LABEL_PACKAGE_TYPE,
class: ['text-center'],
},
{
key: 'created_at',
label: s__('Created'),
key: LIST_KEY_CREATED_AT,
label: LIST_LABEL_CREATED_AT,
class: this.showActions ? ['text-center'] : ['text-right'],
},
];
].filter(f => f.key !== LIST_KEY_PROJECT || this.isGroupPage);
},
headerFields() {
const actions = {
key: 'actions',
label: '',
key: LIST_KEY_ACTIONS,
label: LIST_LABEL_ACTIONS,
tdClass: ['text-right'],
};
return this.showActions ? [...this.sortableFields, actions] : this.sortableFields;
},
modalAction() {
return s__('PackageRegistry|Delete Package');
return s__('PackageRegistry|Delete package');
},
deletePackageDescription() {
if (!this.itemToBeDeleted) {
......@@ -107,20 +122,24 @@ export default {
s__(
'PackageRegistry|You are about to delete <b>%{packageName}</b>, this operation is irreversible, are you sure?',
),
{ packageName: this.itemToBeDeleted.name },
{ packageName: `${this.itemToBeDeleted.name}:${this.itemToBeDeleted.version}` },
false,
);
},
},
methods: {
onDirectionChange() {},
onSortItemClick() {},
setItemToBeDeleted({ name, id }) {
this.itemToBeDeleted = { name, id };
onDirectionChange() {
// to be connected to the sorting api when the api is ready
},
onSortItemClick() {
// to be connected to the sorting api when the api is ready
},
setItemToBeDeleted(item) {
this.itemToBeDeleted = { ...item };
this.$refs.packageListDeleteModal.show();
},
deleteItemConfirmation() {
// this is going to be connected to vuex action
this.$emit('package:delete', this.itemToBeDeleted.id);
this.itemToBeDeleted = null;
},
deleteItemCanceled() {
......@@ -160,11 +179,17 @@ export default {
>
<template #name="{value}">
<div ref="col-name" class="flex-truncate-parent">
<a href="/asd/lol" class="flex-truncate-child" data-qa-selector="package_link">
<a href="#" class="flex-truncate-child" data-qa-selector="package_link">
{{ value }}
</a>
</div>
</template>
<template #project="{value}">
<div ref="col-project" class="flex-truncate-parent">
<a :href="value" class="flex-truncate-child"> {{ value }} </a>
</div>
</template>
<template #version="{value}">
{{ value }}
</template>
......
<script>
import { GlEmptyState } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import PackageList from './packages_list.vue';
export default {
components: {
GlEmptyState,
GlLoadingIcon,
PackageList,
},
props: {
projectId: {
type: String,
required: false,
default: '',
},
groupId: {
type: String,
required: false,
default: '',
},
canDestroyPackage: {
type: Boolean,
required: false,
default: false,
},
emptyListIllustration: {
type: String,
required: true,
},
emptyListHelpUrl: {
type: String,
required: true,
},
},
computed: {
...mapState({
isLoading: 'isLoading',
resourceId: state => state.config.resourceId,
emptyListIllustration: state => state.config.emptyListIllustration,
emptyListHelpUrl: state => state.config.emptyListHelpUrl,
}),
emptyListText() {
return sprintf(
s__(
......@@ -47,11 +30,24 @@ export default {
);
},
},
mounted() {
this.requestPackagesList();
},
methods: {
...mapActions(['requestPackagesList', 'requestDeletePackage']),
onPageChanged(page) {
return this.requestPackagesList({ page });
},
onPackageDeleteRequest(packageId) {
return this.requestDeletePackage({ projectId: this.resourceId, packageId });
},
},
};
</script>
<template>
<package-list :can-destroy-package="canDestroyPackage">
<gl-loading-icon v-if="isLoading" />
<package-list v-else @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
<template #empty-state>
<gl-empty-state
:title="s__('PackageRegistry|There are no packages yet')"
......
......@@ -5,3 +5,23 @@ export const FETCH_PACKAGES_LIST_ERROR_MESSAGE = __(
);
export const FETCH_PACKAGE_ERROR_MESSAGE = __('Something went wrong while fetching the package.');
export const DELETE_PACKAGE_ERROR_MESSAGE = __('Something went wrong while deleting the package.');
export const DELETE_PACKAGE_SUCCESS_MESSAGE = __('Package deleted successfully');
export const DEFAULT_PAGE = 1;
export const DEFAULT_PAGE_SIZE = 20;
export const GROUP_PAGE_TYPE = 'groups';
export const LIST_KEY_NAME = 'name';
export const LIST_KEY_PROJECT = 'project';
export const LIST_KEY_VERSION = 'version';
export const LIST_KEY_PACKAGE_TYPE = 'package_type';
export const LIST_KEY_CREATED_AT = 'created_at';
export const LIST_KEY_ACTIONS = 'actions';
export const LIST_LABEL_NAME = __('Name');
export const LIST_LABEL_PROJECT = __('Project');
export const LIST_LABEL_VERSION = __('Version');
export const LIST_LABEL_PACKAGE_TYPE = __('Type');
export const LIST_LABEL_CREATED_AT = __('Created');
export const LIST_LABEL_ACTIONS = '';
import Vue from 'vue';
import PackagesListApp from './components/packages_list_app.vue';
import Translate from '~/vue_shared/translate';
import { createStore } from './stores';
import PackagesListApp from './components/packages_list_app.vue';
Vue.use(Translate);
export default () =>
new Vue({
el: '#js-vue-packages-list',
export default () => {
const el = document.getElementById('js-vue-packages-list');
const store = createStore();
store.dispatch('setInitialState', el.dataset);
return new Vue({
el,
store,
components: {
PackagesListApp,
},
data() {
const {
dataset: { projectId, groupId, emptyListIllustration, emptyListHelpUrl, canDestroyPackage },
} = document.querySelector(this.$options.el);
return {
packageListAttrs: {
projectId,
groupId,
emptyListIllustration,
emptyListHelpUrl,
canDestroyPackage,
},
};
},
render(createElement) {
return createElement('packages-list-app', {
props: {
...this.packageListAttrs,
},
});
return createElement('packages-list-app');
},
});
};
import Api from '~/api';
import Api from 'ee/api';
import createFlash from '~/flash';
import * as types from './mutation_types';
import { FETCH_PACKAGES_LIST_ERROR_MESSAGE, DELETE_PACKAGE_ERROR_MESSAGE } from '../constants';
import {
FETCH_PACKAGES_LIST_ERROR_MESSAGE,
DELETE_PACKAGE_ERROR_MESSAGE,
DELETE_PACKAGE_SUCCESS_MESSAGE,
DEFAULT_PAGE,
DEFAULT_PAGE_SIZE,
} from '../constants';
export const setProjectId = ({ commit }, data) => commit(types.SET_PROJECT_ID, data);
export const setUserCanDelete = ({ commit }, data) => commit(types.SET_USER_CAN_DELETE, data);
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
export const setLoading = ({ commit }, data) => commit(types.SET_MAIN_LOADING, data);
export const receivePackagesListSuccess = ({ commit }, data) =>
export const receivePackagesListSuccess = ({ commit }, { data, headers }) => {
commit(types.SET_PACKAGE_LIST_SUCCESS, data);
commit(types.SET_PAGINATION, headers);
};
export const requestPackagesList = ({ dispatch, state }) => {
export const requestPackagesList = ({ dispatch, state }, pagination = {}) => {
dispatch('setLoading', true);
const { page, perPage } = state.pagination;
return Api.projectPackages(state.projectId, { params: { page, perPage } })
.then(({ data }) => {
dispatch('receivePackagesListSuccess', data);
const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination;
const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages';
return Api[apiMethod](state.config.resourceId, { params: { page, per_page: perPage } })
.then(({ data, headers }) => {
dispatch('receivePackagesListSuccess', { data, headers });
})
.catch(() => {
createFlash(FETCH_PACKAGES_LIST_ERROR_MESSAGE);
......@@ -30,7 +37,10 @@ export const requestPackagesList = ({ dispatch, state }) => {
export const requestDeletePackage = ({ dispatch }, { projectId, packageId }) => {
dispatch('setLoading', true);
return Api.deleteProjectPackage(projectId, packageId)
.then(() => dispatch('fetchPackages'))
.then(() => {
dispatch('requestPackagesList');
createFlash(DELETE_PACKAGE_SUCCESS_MESSAGE, 'success');
})
.catch(() => {
createFlash(DELETE_PACKAGE_ERROR_MESSAGE);
})
......
export const SET_PROJECT_ID = 'SET_PROJECT_ID';
export const SET_USER_CAN_DELETE = 'SET_USER_CAN_DELETE';
export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
export const SET_PACKAGE_LIST = 'SET_PACKAGE_LIST';
export const SET_PACKAGE_LIST_SUCCESS = 'SET_PACKAGE_LIST_SUCCESS';
export const SET_PAGINATION = 'SET_PAGINATION';
export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
import _ from 'underscore';
import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { GROUP_PAGE_TYPE } from '../constants';
export default {
[types.SET_PROJECT_ID](state, projectId) {
state.projectId = projectId;
[types.SET_INITIAL_STATE](state, config) {
state.config = {
...config,
isGroupPage: config.pageType === GROUP_PAGE_TYPE,
canDestroyPackage: !(
_.isNull(config.canDestroyPackage) || _.isUndefined(config.canDestroyPackage)
),
};
},
[types.SET_USER_CAN_DELETE](state, userCanDelete) {
state.userCanDelete = userCanDelete;
},
[types.SET_PACKAGE_LIST](state, packages) {
[types.SET_PACKAGE_LIST_SUCCESS](state, packages) {
state.packages = packages;
},
......@@ -18,7 +22,7 @@ export default {
state.isLoading = isLoading;
},
[types.SET_PAGINATION](state, { headers }) {
[types.SET_PAGINATION](state, headers) {
const normalizedHeaders = normalizeHeaders(headers);
state.pagination = parseIntPagination(normalizedHeaders);
},
......
export default () => ({
/**
* Determine if the component is loading data from the API
*/
isLoading: false,
/** project id used to fetch data */
projectId: null,
userCanDelete: false, // controls the delete buttons in the list
/**
* configuration object, set once at store creation with the following structure
* {
* resourceId: String,
* pageType: String,
* userCanDelete: Boolean,
* emptyListIllustration: String,
* emptyListHelpUrl: String
* }
*/
config: {},
/**
* Each object in `packages` has the following structure:
* {
......@@ -13,5 +24,13 @@ export default () => ({
* }
*/
packages: [],
/**
* Pagination object has the following structure:
* {
* perPage: Number,
* page: Number
* total: Number
* }
*/
pagination: {},
});
import initPackageList from 'ee/packages/list/packages_list_app_bundle';
document.addEventListener('DOMContentLoaded', initPackageList);
document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('js-vue-packages-list')) {
initPackageList();
}
});
import initPackageList from 'ee/packages/list/packages_list_app_bundle';
document.addEventListener('DOMContentLoaded', initPackageList);
document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('js-vue-packages-list')) {
initPackageList();
}
});
......@@ -3,7 +3,8 @@
- if vue_package_list_enabled_for?(@group)
.row
.col-12
#js-vue-packages-list{ data: { group_id: @group.id,
#js-vue-packages-list{ data: { resource_id: @group.id,
page_type: 'groups',
empty_list_help_url: help_page_path('administration/packages/index'),
empty_list_illustration: image_path('illustrations/no-packages.svg') } }
......
......@@ -4,7 +4,8 @@
- if vue_package_list_enabled_for?(@project)
.row
.col-12
#js-vue-packages-list{ data: { project_id: @project.id,
#js-vue-packages-list{ data: { resource_id: @project.id,
page_type: 'projects',
can_destroy_package: can_destroy_package,
empty_list_help_url: help_page_path('administration/packages/index'),
empty_list_illustration: image_path('illustrations/no-packages.svg') } }
......
......@@ -216,17 +216,32 @@ describe('Api', () => {
describe('packages', () => {
const projectId = 'project_a';
const packageId = 'package_b';
const apiResponse = [{ id: 1, name: 'foo' }];
describe('groupPackages', () => {
const groupId = 'group_a';
it('fetch all group packages', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/packages`;
jest.spyOn(axios, 'get');
mock.onGet(expectedUrl).replyOnce(200, apiResponse);
return Api.groupPackages(groupId).then(({ data }) => {
expect(data).toEqual(apiResponse);
expect(axios.get).toHaveBeenCalledWith(expectedUrl, {});
});
});
});
describe('projectPackages', () => {
it('fetch all project packages', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/packages`;
const apiResponse = [{ id: 1, name: 'foo' }];
jest.spyOn(axios, 'get');
mock.onGet(expectedUrl).replyOnce(200, apiResponse);
return Api.projectPackages(projectId).then(({ data }) => {
expect(data).toEqual(apiResponse);
expect(axios.get).toHaveBeenCalledWith(expectedUrl);
expect(axios.get).toHaveBeenCalledWith(expectedUrl, {});
});
});
});
......@@ -242,8 +257,6 @@ describe('Api', () => {
describe('projectPackage', () => {
it('fetch package details', () => {
const expectedUrl = `foo`;
const apiResponse = { id: 1, name: 'foo' };
jest.spyOn(Api, 'buildProjectPackageUrl').mockReturnValue(expectedUrl);
jest.spyOn(axios, 'get');
mock.onGet(expectedUrl).replyOnce(200, apiResponse);
......@@ -258,14 +271,13 @@ describe('Api', () => {
describe('deleteProjectPackage', () => {
it('delete a package', () => {
const expectedUrl = `foo`;
const apiResponse = true;
jest.spyOn(Api, 'buildProjectPackageUrl').mockReturnValue(expectedUrl);
jest.spyOn(axios, 'delete');
mock.onDelete(expectedUrl).replyOnce(200, apiResponse);
mock.onDelete(expectedUrl).replyOnce(200, true);
return Api.deleteProjectPackage(projectId, packageId).then(({ data }) => {
expect(data).toEqual(apiResponse);
expect(data).toEqual(true);
expect(axios.delete).toHaveBeenCalledWith(expectedUrl);
});
});
......
......@@ -50,7 +50,7 @@ exports[`packages_list renders 1`] = `
>
<th
aria-colindex="1"
class=""
class="text-left"
role="columnheader"
scope="col"
>
......@@ -58,7 +58,7 @@ exports[`packages_list renders 1`] = `
</th>
<th
aria-colindex="2"
class=""
class="text-center"
role="columnheader"
scope="col"
>
......@@ -66,7 +66,7 @@ exports[`packages_list renders 1`] = `
</th>
<th
aria-colindex="3"
class=""
class="text-center"
role="columnheader"
scope="col"
>
......@@ -74,7 +74,7 @@ exports[`packages_list renders 1`] = `
</th>
<th
aria-colindex="4"
class=""
class="text-center"
role="columnheader"
scope="col"
>
......@@ -103,7 +103,7 @@ exports[`packages_list renders 1`] = `
>
<td
aria-colindex="1"
class="w-25"
class="text-left"
role="cell"
>
<div
......@@ -112,7 +112,7 @@ exports[`packages_list renders 1`] = `
<a
class="flex-truncate-child"
data-qa-selector="package_link"
href="/asd/lol"
href="#"
>
Test package
......@@ -122,7 +122,7 @@ exports[`packages_list renders 1`] = `
</td>
<td
aria-colindex="2"
class=""
class="text-center"
role="cell"
>
......@@ -131,7 +131,7 @@ exports[`packages_list renders 1`] = `
</td>
<td
aria-colindex="3"
class=""
class="text-center"
role="cell"
>
......@@ -140,7 +140,7 @@ exports[`packages_list renders 1`] = `
</td>
<td
aria-colindex="4"
class=""
class="text-center"
role="cell"
>
<timeagotooltip-stub
......@@ -172,7 +172,7 @@ exports[`packages_list renders 1`] = `
>
<td
aria-colindex="1"
class="w-25"
class="text-left"
role="cell"
>
<div
......@@ -181,7 +181,7 @@ exports[`packages_list renders 1`] = `
<a
class="flex-truncate-child"
data-qa-selector="package_link"
href="/asd/lol"
href="#"
>
@Test/package
......@@ -191,7 +191,7 @@ exports[`packages_list renders 1`] = `
</td>
<td
aria-colindex="2"
class=""
class="text-center"
role="cell"
>
......@@ -200,7 +200,7 @@ exports[`packages_list renders 1`] = `
</td>
<td
aria-colindex="3"
class=""
class="text-center"
role="cell"
>
......@@ -209,7 +209,7 @@ exports[`packages_list renders 1`] = `
</td>
<td
aria-colindex="4"
class=""
class="text-center"
role="cell"
>
<timeagotooltip-stub
......@@ -253,9 +253,9 @@ exports[`packages_list renders 1`] = `
labelprevpage="Go to previous page"
limits="[object Object]"
nexttext="Next ›"
perpage="20"
perpage="1"
prevtext="‹ Prev"
totalitems="100"
totalitems="1"
value="1"
/>
......
......@@ -4,21 +4,38 @@ import PackageListApp from 'ee/packages/list/components/packages_list_app.vue';
describe('packages_list_app', () => {
let wrapper;
const emptyListHelpUrl = 'helpUrl';
const findGlEmptyState = (w = wrapper) => w.find({ name: 'gl-empty-state-stub' });
const findListComponent = (w = wrapper) => w.find({ name: 'package-list' });
const findLoadingComponent = (w = wrapper) => w.find({ name: 'gl-loading-icon' });
beforeEach(() => {
wrapper = shallowMount(PackageListApp, {
propsData: {
projectId: '1',
emptyListIllustration: 'helpSvg',
emptyListHelpUrl,
const componentConfig = {
stubs: {
'package-list': {
name: 'package-list',
template: '<div><slot name="empty-state"></slot></div>',
},
stubs: {
'package-list': '<div><slot name="empty-state"></slot></div>',
GlEmptyState: { ...GlEmptyState, name: 'gl-empty-state-stub' },
},
});
GlEmptyState: { ...GlEmptyState, name: 'gl-empty-state-stub' },
'gl-loading-icon': { name: 'gl-loading-icon', template: '<div>loading</div>' },
},
computed: {
isLoading: () => false,
emptyListIllustration: () => 'helpSvg',
emptyListHelpUrl: () => emptyListHelpUrl,
resourceId: () => 'project_id',
},
methods: {
requestPackagesList: jest.fn(),
requestDeletePackage: jest.fn(),
setProjectId: jest.fn(),
setGroupId: jest.fn(),
setUserCanDelete: jest.fn(),
},
};
beforeEach(() => {
wrapper = shallowMount(PackageListApp, componentConfig);
});
afterEach(() => {
......@@ -29,6 +46,21 @@ describe('packages_list_app', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('when isLoading is true', () => {
beforeEach(() => {
wrapper = shallowMount(PackageListApp, {
...componentConfig,
computed: {
isLoading: () => true,
},
});
});
it('shows the loading component', () => {
const loader = findLoadingComponent();
expect(loader.exists()).toBe(true);
});
});
it('generate the correct empty list link', () => {
const emptyState = findGlEmptyState();
const link = emptyState.find('a');
......@@ -37,4 +69,19 @@ describe('packages_list_app', () => {
`"<a href=\\"${emptyListHelpUrl}\\" target=\\"_blank\\">publish and share your packages</a>"`,
);
});
it('call requestPackagesList on page:changed', () => {
const list = findListComponent();
list.vm.$emit('page:changed', 1);
expect(componentConfig.methods.requestPackagesList).toHaveBeenCalledWith({ page: 1 });
});
it('call requestDeletePackage on package:delete', () => {
const list = findListComponent();
list.vm.$emit('package:delete', 1);
expect(componentConfig.methods.requestDeletePackage).toHaveBeenCalledWith({
projectId: 'project_id',
packageId: 1,
});
});
});
import Vue from 'vue';
import _ from 'underscore';
import { shallowMount } from '@vue/test-utils';
import { GlTable } from '@gitlab/ui';
import PackagesList from 'ee/packages/list/components/packages_list.vue';
......@@ -13,17 +14,20 @@ describe('packages_list', () => {
const findPackageListPagination = (w = wrapper) => w.find({ ref: 'packageListPagination' });
const findPackageListDeleteModal = (w = wrapper) => w.find({ ref: 'packageListDeleteModal' });
const findSortingItems = (w = wrapper) => w.findAll({ name: 'sorting-item-stub' });
const findFirstProjectColumn = (w = wrapper) => w.find({ ref: 'col-project' });
const defaultShallowMountOptions = {
propsData: {
canDestroyPackage: true,
},
stubs: {
GlTable,
GlSortingItem: { name: 'sorting-item-stub', template: '<div><slot></slot></div>' },
},
computed: {
list: () => [...packageList],
perPage: () => 1,
totalItems: () => 1,
page: () => 1,
canDestroyPackage: () => true,
isGroupPage: () => false,
},
};
......@@ -43,6 +47,24 @@ describe('packages_list', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('when is isGroupPage', () => {
beforeEach(() => {
wrapper = shallowMount(PackagesList, {
...defaultShallowMountOptions,
computed: {
...defaultShallowMountOptions.computed,
canDestroyPackage: () => false,
isGroupPage: () => true,
},
});
});
it('has project field', () => {
const projectColumn = findFirstProjectColumn();
expect(projectColumn.exists()).toBe(true);
});
});
it('contains a sorting component', () => {
const sorting = findPackageListSorting();
expect(sorting.exists()).toBe(true);
......@@ -63,8 +85,14 @@ describe('packages_list', () => {
});
describe('when user can not destroy the package', () => {
beforeEach(() => {
wrapper = shallowMount(PackagesList, {
...defaultShallowMountOptions,
computed: { ...defaultShallowMountOptions.computed, canDestroyPackage: () => false },
});
});
it('does not show the action column', () => {
wrapper.setProps({ canDestroyPackage: false });
const action = findFirstActionColumn();
expect(action.exists()).toBe(false);
});
......@@ -79,19 +107,19 @@ describe('packages_list', () => {
it('shows the correct deletePackageDescription', () => {
expect(wrapper.vm.deletePackageDescription).toEqual('');
wrapper.setData({ itemToBeDeleted: { name: 'foo' } });
expect(wrapper.vm.deletePackageDescription).toEqual(
'You are about to delete <b>foo</b>, this operation is irreversible, are you sure?',
wrapper.setData({ itemToBeDeleted: { name: 'foo', version: '1.0.10-beta' } });
expect(wrapper.vm.deletePackageDescription).toMatchInlineSnapshot(
`"You are about to delete <b>foo:1.0.10-beta</b>, this operation is irreversible, are you sure?"`,
);
});
it('delete button set itemToBeDeleted and open the modal', () => {
wrapper.vm.$refs.packageListDeleteModal.show = jest.fn();
const [{ name, id }] = packageList.slice(-1);
const item = _.last(packageList);
const action = findFirstActionColumn();
action.vm.$emit('click');
return Vue.nextTick().then(() => {
expect(wrapper.vm.itemToBeDeleted).toEqual({ id, name });
expect(wrapper.vm.itemToBeDeleted).toEqual(item);
expect(wrapper.vm.$refs.packageListDeleteModal.show).toHaveBeenCalled();
});
});
......@@ -101,6 +129,11 @@ describe('packages_list', () => {
wrapper.vm.deleteItemConfirmation();
expect(wrapper.vm.itemToBeDeleted).toEqual(null);
});
it('deleteItemConfirmation emit package:delete', () => {
wrapper.setData({ itemToBeDeleted: { id: 2 } });
wrapper.vm.deleteItemConfirmation();
expect(wrapper.emitted('package:delete')).toEqual([[2]]);
});
it('deleteItemCanceled resets itemToBeDeleted', () => {
wrapper.setData({ itemToBeDeleted: 1 });
......@@ -135,5 +168,9 @@ describe('packages_list', () => {
const sortingItems = findSortingItems();
expect(sortingItems.length).toEqual(wrapper.vm.sortableFields.length);
});
it('emits page:changed events when the page changes', () => {
wrapper.vm.currentPage = 2;
expect(wrapper.emitted('page:changed')).toEqual([[2]]);
});
});
});
......@@ -2,40 +2,57 @@ import * as actions from 'ee/packages/list/stores/actions';
import * as types from 'ee/packages/list/stores/mutation_types';
import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
import Api from '~/api';
import Api from 'ee/api';
jest.mock('~/flash.js');
jest.mock('~/api.js');
jest.mock('ee/api.js');
describe('Actions Package list store', () => {
let state;
beforeEach(() => {
state = {
pagination: {
page: 1,
perPage: 10,
},
};
});
const headers = 'bar';
describe('requestPackagesList', () => {
beforeEach(() => {
Api.projectPackages = jest.fn().mockResolvedValue({ data: 'foo' });
Api.projectPackages = jest.fn().mockResolvedValue({ data: 'foo', headers });
Api.groupPackages = jest.fn().mockResolvedValue({ data: 'baz', headers });
});
it('should dispatch the correct actions', done => {
it('should fetch the project packages list when isGroupPage is false', done => {
testAction(
actions.requestPackagesList,
null,
state,
undefined,
{ config: { isGroupPage: false, resourceId: 1 } },
[],
[
{ type: 'setLoading', payload: true },
{ type: 'receivePackagesListSuccess', payload: 'foo' },
{ type: 'receivePackagesListSuccess', payload: { data: 'foo', headers } },
{ type: 'setLoading', payload: false },
],
done,
() => {
expect(Api.projectPackages).toHaveBeenCalledWith(1, {
params: { page: 1, per_page: 20 },
});
done();
},
);
});
it('should fetch the group packages list when isGroupPage is true', done => {
testAction(
actions.requestPackagesList,
undefined,
{ config: { isGroupPage: true, resourceId: 2 } },
[],
[
{ type: 'setLoading', payload: true },
{ type: 'receivePackagesListSuccess', payload: { data: 'baz', headers } },
{ type: 'setLoading', payload: false },
],
() => {
expect(Api.groupPackages).toHaveBeenCalledWith(2, {
params: { page: 1, per_page: 20 },
});
done();
},
);
});
......@@ -43,8 +60,8 @@ describe('Actions Package list store', () => {
Api.projectPackages = jest.fn().mockRejectedValue();
testAction(
actions.requestPackagesList,
null,
state,
undefined,
{ config: { isGroupPage: false, resourceId: 2 } },
[],
[{ type: 'setLoading', payload: true }, { type: 'setLoading', payload: false }],
() => {
......@@ -57,37 +74,29 @@ describe('Actions Package list store', () => {
describe('receivePackagesListSuccess', () => {
it('should set received packages', done => {
const data = 'foo';
testAction(
actions.receivePackagesListSuccess,
'foo',
state,
[{ type: types.SET_PACKAGE_LIST_SUCCESS, payload: 'foo' }],
{ data, headers },
null,
[
{ type: types.SET_PACKAGE_LIST_SUCCESS, payload: data },
{ type: types.SET_PAGINATION, payload: headers },
],
[],
done,
);
});
});
describe('setProjectId', () => {
it('should commit setProjectId', done => {
describe('setInitialState', () => {
it('should commit setInitialState', done => {
testAction(
actions.setProjectId,
actions.setInitialState,
'1',
state,
[{ type: types.SET_PROJECT_ID, payload: '1' }],
[],
done,
);
});
});
describe('setUserCanDelete', () => {
it('should commit setUserCanDelete', done => {
testAction(
actions.setUserCanDelete,
true,
state,
[{ type: types.SET_USER_CAN_DELETE, payload: true }],
null,
[{ type: types.SET_INITIAL_STATE, payload: '1' }],
[],
done,
);
......@@ -99,7 +108,7 @@ describe('Actions Package list store', () => {
testAction(
actions.setLoading,
true,
state,
null,
[{ type: types.SET_MAIN_LOADING, payload: true }],
[],
done,
......@@ -122,7 +131,7 @@ describe('Actions Package list store', () => {
[],
[
{ type: 'setLoading', payload: true },
{ type: 'fetchPackages' },
{ type: 'requestPackagesList' },
{ type: 'setLoading', payload: false },
],
done,
......
......@@ -10,29 +10,35 @@ describe('Mutations Registry Store', () => {
mockState = createState();
});
describe('SET_PROJECT_ID', () => {
it('should set the project id', () => {
const expectedState = { ...mockState, projectId: 'foo' };
mutations[types.SET_PROJECT_ID](mockState, 'foo');
describe('SET_INITIAL_STATE', () => {
it('should set the initial state', () => {
const config = {
resourceId: '1',
pageType: 'groups',
userCanDelete: '',
emptyListIllustration: 'foo',
emptyListHelpUrl: 'baz',
};
expect(mockState.projectId).toEqual(expectedState.projectId);
});
});
const expectedState = {
...mockState,
config: {
...config,
isGroupPage: true,
canDestroyPackage: true,
},
};
mutations[types.SET_INITIAL_STATE](mockState, config);
describe('SET_USER_CAN_DELETE', () => {
it('should set the userCanDelete', () => {
const expectedState = { ...mockState, userCanDelete: true };
mutations[types.SET_USER_CAN_DELETE](mockState, true);
expect(mockState.userCanDelete).toEqual(expectedState.userCanDelete);
expect(mockState.projectId).toEqual(expectedState.projectId);
});
});
describe('SET_PACKAGE_LIST', () => {
describe('SET_PACKAGE_LIST_SUCCESS', () => {
it('should set a packages list', () => {
const payload = [npmPackage, mavenPackage];
const expectedState = { ...mockState, packages: payload };
mutations[types.SET_PACKAGE_LIST](mockState, payload);
mutations[types.SET_PACKAGE_LIST_SUCCESS](mockState, payload);
expect(mockState.packages).toEqual(expectedState.packages);
});
......@@ -53,7 +59,7 @@ describe('Mutations Registry Store', () => {
commonUtils.parseIntPagination = jest.fn().mockReturnValue(mockPagination);
});
it('should set a parsed pagination', () => {
mutations[types.SET_PAGINATION](mockState, { headers: 'foo' });
mutations[types.SET_PAGINATION](mockState, 'foo');
expect(commonUtils.normalizeHeaders).toHaveBeenCalledWith('foo');
expect(commonUtils.parseIntPagination).toHaveBeenCalledWith('baz');
expect(mockState.pagination).toEqual(mockPagination);
......
......@@ -11947,6 +11947,9 @@ msgstr ""
msgid "Owner"
msgstr ""
msgid "Package deleted successfully"
msgstr ""
msgid "Package information"
msgstr ""
......@@ -11965,10 +11968,10 @@ msgstr ""
msgid "PackageRegistry|Copy yarn setup command"
msgstr ""
msgid "PackageRegistry|Delete Package"
msgid "PackageRegistry|Delete Package Version"
msgstr ""
msgid "PackageRegistry|Delete Package Version"
msgid "PackageRegistry|Delete package"
msgstr ""
msgid "PackageRegistry|Installation"
......
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