Commit a570359c authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'move-packages-to-axios' into 'master'

Move package deletion from rails-js to axios

See merge request gitlab-org/gitlab!41668
parents b8e67cb6 817b10ab
...@@ -25,8 +25,9 @@ import { numberToHumanSize } from '~/lib/utils/number_utils'; ...@@ -25,8 +25,9 @@ import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import FileIcon from '~/vue_shared/components/file_icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { PackageType, TrackingActions } from '../../shared/constants'; import { PackageType, TrackingActions, SHOW_DELETE_SUCCESS_ALERT } from '../../shared/constants';
import { packageTypeToTrackCategory } from '../../shared/utils'; import { packageTypeToTrackCategory } from '../../shared/utils';
import { objectToQueryString } from '~/lib/utils/common_utils';
export default { export default {
name: 'PackagesApp', name: 'PackagesApp',
...@@ -62,17 +63,15 @@ export default { ...@@ -62,17 +63,15 @@ export default {
'packageFiles', 'packageFiles',
'isLoading', 'isLoading',
'canDelete', 'canDelete',
'destroyPath',
'svgPath', 'svgPath',
'npmPath', 'npmPath',
'npmHelpPath', 'npmHelpPath',
'projectListUrl',
'groupListUrl',
]), ]),
isValidPackage() { isValidPackage() {
return Boolean(this.packageEntity.name); return Boolean(this.packageEntity.name);
}, },
canDeletePackage() {
return this.canDelete && this.destroyPath;
},
filesTableRows() { filesTableRows() {
return this.packageFiles.map(x => ({ return this.packageFiles.map(x => ({
name: x.file_name, name: x.file_name,
...@@ -100,7 +99,7 @@ export default { ...@@ -100,7 +99,7 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['fetchPackageVersions']), ...mapActions(['deletePackage', 'fetchPackageVersions']),
formatSize(size) { formatSize(size) {
return numberToHumanSize(size); return numberToHumanSize(size);
}, },
...@@ -112,6 +111,16 @@ export default { ...@@ -112,6 +111,16 @@ export default {
this.fetchPackageVersions(); this.fetchPackageVersions();
} }
}, },
async confirmPackageDeletion() {
this.track(TrackingActions.DELETE_PACKAGE);
await this.deletePackage();
const returnTo =
!this.groupListUrl || document.referrer.includes(this.projectName)
? this.projectListUrl
: this.groupListUrl; // to avoid security issue url are supplied from backend
const modalQuery = objectToQueryString({ [SHOW_DELETE_SUCCESS_ALERT]: true });
window.location.replace(`${returnTo}?${modalQuery}`);
},
}, },
i18n: { i18n: {
deleteModalTitle: s__(`PackageRegistry|Delete Package Version`), deleteModalTitle: s__(`PackageRegistry|Delete Package Version`),
...@@ -150,7 +159,7 @@ export default { ...@@ -150,7 +159,7 @@ export default {
<package-title> <package-title>
<template #delete-button> <template #delete-button>
<gl-button <gl-button
v-if="canDeletePackage" v-if="canDelete"
v-gl-modal="'delete-modal'" v-gl-modal="'delete-modal'"
class="js-delete-button" class="js-delete-button"
variant="danger" variant="danger"
...@@ -272,12 +281,10 @@ export default { ...@@ -272,12 +281,10 @@ export default {
<gl-button @click="cancelDelete">{{ __('Cancel') }}</gl-button> <gl-button @click="cancelDelete">{{ __('Cancel') }}</gl-button>
<gl-button <gl-button
ref="modal-delete-button" ref="modal-delete-button"
data-method="delete"
:to="destroyPath"
variant="danger" variant="danger"
category="primary" category="primary"
data-qa-selector="delete_modal_button" data-qa-selector="delete_modal_button"
@click="track($options.trackingActions.DELETE_PACKAGE)" @click="confirmPackageDeletion"
> >
{{ __('Delete') }} {{ __('Delete') }}
</gl-button> </gl-button>
......
import Api from '~/api'; import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants'; import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default ({ commit, state }) => { export const fetchPackageVersions = ({ commit, state }) => {
commit(types.SET_LOADING, true); commit(types.SET_LOADING, true);
const { project_id, id } = state.packageEntity; const { project_id, id } = state.packageEntity;
...@@ -21,3 +22,13 @@ export default ({ commit, state }) => { ...@@ -21,3 +22,13 @@ export default ({ commit, state }) => {
commit(types.SET_LOADING, false); commit(types.SET_LOADING, false);
}); });
}; };
export const deletePackage = ({
state: {
packageEntity: { project_id, id },
},
}) => {
return Api.deleteProjectPackage(project_id, id).catch(() => {
createFlash(DELETE_PACKAGE_ERROR_MESSAGE);
});
};
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import fetchPackageVersions from './actions'; import * as actions from './actions';
import * as getters from './getters'; import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
...@@ -8,9 +8,7 @@ Vue.use(Vuex); ...@@ -8,9 +8,7 @@ Vue.use(Vuex);
export default (initialState = {}) => export default (initialState = {}) =>
new Vuex.Store({ new Vuex.Store({
actions: { actions,
fetchPackageVersions,
},
getters, getters,
mutations, mutations,
state: { state: {
......
...@@ -2,11 +2,14 @@ ...@@ -2,11 +2,14 @@
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { GlEmptyState, GlTab, GlTabs, GlLink, GlSprintf } from '@gitlab/ui'; import { GlEmptyState, GlTab, GlTabs, GlLink, GlSprintf } from '@gitlab/ui';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import createFlash from '~/flash';
import PackageFilter from './packages_filter.vue'; import PackageFilter from './packages_filter.vue';
import PackageList from './packages_list.vue'; import PackageList from './packages_list.vue';
import PackageSort from './packages_sort.vue'; import PackageSort from './packages_sort.vue';
import { PACKAGE_REGISTRY_TABS } from '../constants'; import { PACKAGE_REGISTRY_TABS, DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
import PackagesComingSoon from '../coming_soon/packages_coming_soon.vue'; import PackagesComingSoon from '../coming_soon/packages_coming_soon.vue';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import { historyReplaceState } from '~/lib/utils/common_utils';
export default { export default {
components: { components: {
...@@ -34,6 +37,7 @@ export default { ...@@ -34,6 +37,7 @@ export default {
}, },
mounted() { mounted() {
this.requestPackagesList(); this.requestPackagesList();
this.checkDeleteAlert();
}, },
methods: { methods: {
...mapActions(['requestPackagesList', 'requestDeletePackage', 'setSelectedType']), ...mapActions(['requestPackagesList', 'requestDeletePackage', 'setSelectedType']),
...@@ -64,6 +68,16 @@ export default { ...@@ -64,6 +68,16 @@ export default {
return s__('PackageRegistry|There are no packages yet'); return s__('PackageRegistry|There are no packages yet');
}, },
checkDeleteAlert() {
const urlParams = new URLSearchParams(window.location.search);
const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
if (showAlert) {
// to be refactored to use gl-alert
createFlash({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, type: 'notice' });
const cleanUrl = window.location.href.split('?')[0];
historyReplaceState(cleanUrl);
}
},
}, },
i18n: { i18n: {
widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'), widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'),
......
...@@ -5,7 +5,6 @@ export const FETCH_PACKAGES_LIST_ERROR_MESSAGE = __( ...@@ -5,7 +5,6 @@ export const FETCH_PACKAGES_LIST_ERROR_MESSAGE = __(
'Something went wrong while fetching the packages list.', 'Something went wrong while fetching the packages list.',
); );
export const FETCH_PACKAGE_ERROR_MESSAGE = __('Something went wrong while fetching the package.'); 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 DELETE_PACKAGE_SUCCESS_MESSAGE = __('Package deleted successfully');
export const DEFAULT_PAGE = 1; export const DEFAULT_PAGE = 1;
......
import Api from '~/api'; import Api from '~/api';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { import {
FETCH_PACKAGES_LIST_ERROR_MESSAGE, FETCH_PACKAGES_LIST_ERROR_MESSAGE,
DELETE_PACKAGE_ERROR_MESSAGE,
DELETE_PACKAGE_SUCCESS_MESSAGE, DELETE_PACKAGE_SUCCESS_MESSAGE,
DEFAULT_PAGE, DEFAULT_PAGE,
DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE,
......
import { __ } from '~/locale';
export const PackageType = { export const PackageType = {
CONAN: 'conan', CONAN: 'conan',
MAVEN: 'maven', MAVEN: 'maven',
...@@ -22,3 +24,6 @@ export const TrackingCategories = { ...@@ -22,3 +24,6 @@ export const TrackingCategories = {
[PackageType.NPM]: 'NpmPackages', [PackageType.NPM]: 'NpmPackages',
[PackageType.CONAN]: 'ConanPackages', [PackageType.CONAN]: 'ConanPackages',
}; };
export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
export const DELETE_PACKAGE_ERROR_MESSAGE = __('Something went wrong while deleting the package.');
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
.col-12 .col-12
#js-vue-packages-detail{ data: { package: package_from_presenter(@package), #js-vue-packages-detail{ data: { package: package_from_presenter(@package),
can_delete: can?(current_user, :destroy_package, @project).to_s, can_delete: can?(current_user, :destroy_package, @project).to_s,
destroy_path: project_package_path(@project, @package),
svg_path: image_path('illustrations/no-packages.svg'), svg_path: image_path('illustrations/no-packages.svg'),
npm_path: package_registry_instance_url(:npm), npm_path: package_registry_instance_url(:npm),
npm_help_path: help_page_path('user/packages/npm_registry/index'), npm_help_path: help_page_path('user/packages/npm_registry/index'),
...@@ -23,4 +22,6 @@ ...@@ -23,4 +22,6 @@
pypi_help_path: help_page_path('user/packages/pypi_repository/index'), pypi_help_path: help_page_path('user/packages/pypi_repository/index'),
composer_path: composer_registry_url(@project&.group&.id), composer_path: composer_registry_url(@project&.group&.id),
composer_help_path: help_page_path('user/packages/composer_repository/index'), composer_help_path: help_page_path('user/packages/composer_repository/index'),
project_name: @project.name} } project_name: @project.name,
project_list_url: project_packages_path(@project),
group_list_url: @project.group ? group_packages_path(@project.group) : ''} }
...@@ -34,12 +34,15 @@ describe('PackagesApp', () => { ...@@ -34,12 +34,15 @@ describe('PackagesApp', () => {
let wrapper; let wrapper;
let store; let store;
const fetchPackageVersions = jest.fn(); const fetchPackageVersions = jest.fn();
const deletePackage = jest.fn();
const defaultProjectName = 'bar';
const { location } = window;
function createComponent({ function createComponent({
packageEntity = mavenPackage, packageEntity = mavenPackage,
packageFiles = mavenFiles, packageFiles = mavenFiles,
isLoading = false, isLoading = false,
oneColumnView = false, projectName = defaultProjectName,
} = {}) { } = {}) {
store = new Vuex.Store({ store = new Vuex.Store({
state: { state: {
...@@ -47,14 +50,15 @@ describe('PackagesApp', () => { ...@@ -47,14 +50,15 @@ describe('PackagesApp', () => {
packageEntity, packageEntity,
packageFiles, packageFiles,
canDelete: true, canDelete: true,
destroyPath: 'destroy-package-path',
emptySvgPath: 'empty-illustration', emptySvgPath: 'empty-illustration',
npmPath: 'foo', npmPath: 'foo',
npmHelpPath: 'foo', npmHelpPath: 'foo',
projectName: 'bar', projectName,
oneColumnView, projectListUrl: 'project_url',
groupListUrl: 'group_url',
}, },
actions: { actions: {
deletePackage,
fetchPackageVersions, fetchPackageVersions,
}, },
getters, getters,
...@@ -95,8 +99,14 @@ describe('PackagesApp', () => { ...@@ -95,8 +99,14 @@ describe('PackagesApp', () => {
const findAdditionalMetadata = () => wrapper.find(AdditionalMetadata); const findAdditionalMetadata = () => wrapper.find(AdditionalMetadata);
const findInstallationCommands = () => wrapper.find(InstallationCommands); const findInstallationCommands = () => wrapper.find(InstallationCommands);
beforeEach(() => {
delete window.location;
window.location = { replace: jest.fn() };
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
window.location = location;
}); });
it('renders the app and displays the package title', () => { it('renders the app and displays the package title', () => {
...@@ -240,44 +250,94 @@ describe('PackagesApp', () => { ...@@ -240,44 +250,94 @@ describe('PackagesApp', () => {
}); });
}); });
describe('tracking', () => { describe('tracking and delete', () => {
let eventSpy; const doDelete = async () => {
let utilSpy; deleteButton().trigger('click');
const category = 'foo'; await wrapper.vm.$nextTick();
modalDeleteButton().trigger('click');
};
describe('delete', () => {
const originalReferrer = document.referrer;
const setReferrer = (value = defaultProjectName) => {
Object.defineProperty(document, 'referrer', {
value,
configurable: true,
});
};
afterEach(() => {
Object.defineProperty(document, 'referrer', {
value: originalReferrer,
configurable: true,
});
});
beforeEach(() => { it('calls the proper vuex action', async () => {
eventSpy = jest.spyOn(Tracking, 'event'); createComponent({ packageEntity: npmPackage });
utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category); await doDelete();
}); expect(deletePackage).toHaveBeenCalled();
});
it('tracking category calls packageTypeToTrackCategory', () => { it('when referrer contains project name calls window.replace with project url', async () => {
createComponent({ packageEntity: conanPackage }); setReferrer();
expect(wrapper.vm.tracking.category).toBe(category); deletePackage.mockResolvedValue();
expect(utilSpy).toHaveBeenCalledWith('conan'); createComponent({ packageEntity: npmPackage });
await doDelete();
await deletePackage();
expect(window.location.replace).toHaveBeenCalledWith(
'project_url?showSuccessDeleteAlert=true',
);
});
it('when referrer does not contain project name calls window.replace with group url', async () => {
setReferrer('baz');
deletePackage.mockResolvedValue();
createComponent({ packageEntity: npmPackage });
await doDelete();
await deletePackage();
expect(window.location.replace).toHaveBeenCalledWith(
'group_url?showSuccessDeleteAlert=true',
);
});
}); });
it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, () => { describe('tracking', () => {
createComponent({ packageEntity: conanPackage }); let eventSpy;
deleteButton().trigger('click'); let utilSpy;
return wrapper.vm.$nextTick().then(() => { const category = 'foo';
modalDeleteButton().trigger('click');
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category);
});
it('tracking category calls packageTypeToTrackCategory', () => {
createComponent({ packageEntity: conanPackage });
expect(wrapper.vm.tracking.category).toBe(category);
expect(utilSpy).toHaveBeenCalledWith('conan');
});
it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, async () => {
createComponent({ packageEntity: npmPackage });
await doDelete();
expect(eventSpy).toHaveBeenCalledWith( expect(eventSpy).toHaveBeenCalledWith(
category, category,
TrackingActions.DELETE_PACKAGE, TrackingActions.DELETE_PACKAGE,
expect.any(Object), expect.any(Object),
); );
}); });
});
it(`file download link call event with ${TrackingActions.PULL_PACKAGE}`, () => { it(`file download link call event with ${TrackingActions.PULL_PACKAGE}`, () => {
createComponent({ packageEntity: conanPackage }); createComponent({ packageEntity: conanPackage });
firstFileDownloadLink().vm.$emit('click'); firstFileDownloadLink().vm.$emit('click');
expect(eventSpy).toHaveBeenCalledWith( expect(eventSpy).toHaveBeenCalledWith(
category, category,
TrackingActions.PULL_PACKAGE, TrackingActions.PULL_PACKAGE,
expect.any(Object), expect.any(Object),
); );
});
}); });
}); });
}); });
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import Api from '~/api'; import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import fetchPackageVersions from '~/packages/details/store/actions'; import { fetchPackageVersions, deletePackage } from '~/packages/details/store/actions';
import * as types from '~/packages/details/store/mutation_types'; import * as types from '~/packages/details/store/mutation_types';
import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages/details/constants'; import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages/details/constants';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
import { npmPackage as packageEntity } from '../../mock_data'; import { npmPackage as packageEntity } from '../../mock_data';
jest.mock('~/flash.js'); jest.mock('~/flash.js');
...@@ -73,4 +74,25 @@ describe('Actions Package details store', () => { ...@@ -73,4 +74,25 @@ describe('Actions Package details store', () => {
); );
}); });
}); });
describe('deletePackage', () => {
it('should call Api.deleteProjectPackage', done => {
Api.deleteProjectPackage = jest.fn().mockResolvedValue();
testAction(deletePackage, undefined, { packageEntity }, [], [], () => {
expect(Api.deleteProjectPackage).toHaveBeenCalledWith(
packageEntity.project_id,
packageEntity.id,
);
done();
});
});
it('should create flash on API error', done => {
Api.deleteProjectPackage = jest.fn().mockRejectedValue();
testAction(deletePackage, undefined, { packageEntity }, [], [], () => {
expect(createFlash).toHaveBeenCalledWith(DELETE_PACKAGE_ERROR_MESSAGE);
done();
});
});
});
}); });
import Vuex from 'vuex'; import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlEmptyState, GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui'; import { GlEmptyState, GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui';
import * as commonUtils from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import PackageListApp from '~/packages/list/components/packages_list_app.vue'; import PackageListApp from '~/packages/list/components/packages_list_app.vue';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -145,4 +152,46 @@ describe('packages_list_app', () => { ...@@ -145,4 +152,46 @@ describe('packages_list_app', () => {
); );
}); });
}); });
describe('delete alert handling', () => {
const { location } = window.location;
const search = `?${SHOW_DELETE_SUCCESS_ALERT}=true`;
beforeEach(() => {
createStore('foo');
jest.spyOn(commonUtils, 'historyReplaceState').mockImplementation(() => {});
delete window.location;
window.location = {
href: `foo_bar_baz${search}`,
search,
};
});
afterEach(() => {
window.location = location;
});
it(`creates a flash if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
mountComponent();
expect(createFlash).toHaveBeenCalledWith({
message: DELETE_PACKAGE_SUCCESS_MESSAGE,
type: 'notice',
});
});
it('calls historyReplaceState with a clean url', () => {
mountComponent();
expect(commonUtils.historyReplaceState).toHaveBeenCalledWith('foo_bar_baz');
});
it(`does nothing if the query string does not contain ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
window.location.search = '';
mountComponent();
expect(createFlash).not.toHaveBeenCalled();
expect(commonUtils.historyReplaceState).not.toHaveBeenCalled();
});
});
}); });
...@@ -5,7 +5,8 @@ import Api from '~/api'; ...@@ -5,7 +5,8 @@ import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import * as actions from '~/packages/list/stores/actions'; import * as actions from '~/packages/list/stores/actions';
import * as types from '~/packages/list/stores/mutation_types'; import * as types from '~/packages/list/stores/mutation_types';
import { MISSING_DELETE_PATH_ERROR, DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/list/constants'; import { MISSING_DELETE_PATH_ERROR } from '~/packages/list/constants';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
jest.mock('~/flash.js'); jest.mock('~/flash.js');
jest.mock('~/api.js'); jest.mock('~/api.js');
......
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