Commit b960066b authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch '13537-allow-users-to-delete-items-from-the-package-file-list' into 'master'

Allow users to delete items from the package file list

See merge request gitlab-org/gitlab!62179
parents a51cd49e 750cca34
......@@ -23,6 +23,8 @@ const Api = {
groupPackagesPath: '/api/:version/groups/:id/packages',
projectPackagesPath: '/api/:version/projects/:id/packages',
projectPackagePath: '/api/:version/projects/:id/packages/:package_id',
projectPackageFilePath:
'/api/:version/projects/:id/packages/:package_id/package_files/:package_file_id',
groupProjectsPath: '/api/:version/groups/:id/projects.json',
groupSharePath: '/api/:version/groups/:id/share',
projectsPath: '/api/:version/projects.json',
......@@ -124,6 +126,15 @@ const Api = {
return axios.delete(url);
},
deleteProjectPackageFile(projectId, packageId, fileId) {
const url = Api.buildUrl(this.projectPackageFilePath)
.replace(':id', projectId)
.replace(':package_id', packageId)
.replace(':package_file_id', fileId);
return axios.delete(url);
},
containerRegistryDetails(registryId, options = {}) {
const url = Api.buildUrl(this.containerRegistryDetailsPath).replace(':id', registryId);
return axios.get(url, options);
......
......@@ -13,7 +13,7 @@ import {
import { mapActions, mapState } from 'vuex';
import { objectToQueryString } from '~/lib/utils/common_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
import PackageListRow from '../../shared/components/package_list_row.vue';
import PackagesListLoader from '../../shared/components/packages_list_loader.vue';
......@@ -51,6 +51,11 @@ export default {
},
mixins: [Tracking.mixin()],
trackingActions: { ...TrackingActions },
data() {
return {
fileToDelete: null,
};
},
computed: {
...mapState([
'projectName',
......@@ -86,13 +91,10 @@ export default {
},
},
methods: {
...mapActions(['deletePackage', 'fetchPackageVersions']),
...mapActions(['deletePackage', 'fetchPackageVersions', 'deletePackageFile']),
formatSize(size) {
return numberToHumanSize(size);
},
cancelDelete() {
this.$refs.deleteModal.hide();
},
getPackageVersions() {
if (!this.packageEntity.versions) {
this.fetchPackageVersions();
......@@ -108,12 +110,43 @@ export default {
const modalQuery = objectToQueryString({ [SHOW_DELETE_SUCCESS_ALERT]: true });
window.location.replace(`${returnTo}?${modalQuery}`);
},
handleFileDelete(file) {
this.track(TrackingActions.REQUEST_DELETE_PACKAGE_FILE);
this.fileToDelete = { ...file };
this.$refs.deleteFileModal.show();
},
confirmFileDelete() {
this.track(TrackingActions.DELETE_PACKAGE_FILE);
this.deletePackageFile(this.fileToDelete.id);
this.fileToDelete = null;
},
},
i18n: {
deleteModalTitle: s__(`PackageRegistry|Delete Package Version`),
deleteModalContent: s__(
`PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?`,
),
deleteFileModalTitle: s__(`PackageRegistry|Delete Package File`),
deleteFileModalContent: s__(
`PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?`,
),
},
modal: {
packageDeletePrimaryAction: {
text: __('Delete'),
attributes: [
{ variant: 'danger' },
{ category: 'primary' },
{ 'data-qa-selector': 'delete_modal_button' },
],
},
fileDeletePrimaryAction: {
text: __('Delete'),
attributes: [{ variant: 'danger' }, { category: 'primary' }],
},
cancelAction: {
text: __('Cancel'),
},
},
};
</script>
......@@ -159,7 +192,9 @@ export default {
<package-files
v-if="showFiles"
:package-files="packageFiles"
:can-delete="canDelete"
@download-file="track($options.trackingActions.PULL_PACKAGE)"
@delete-file="handleFileDelete"
/>
</gl-tab>
......@@ -210,7 +245,15 @@ export default {
</gl-tab>
</gl-tabs>
<gl-modal ref="deleteModal" class="js-delete-modal" modal-id="delete-modal">
<gl-modal
ref="deleteModal"
class="js-delete-modal"
modal-id="delete-modal"
:action-primary="$options.modal.packageDeletePrimaryAction"
:action-cancel="$options.modal.cancelAction"
@primary="confirmPackageDeletion"
@canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE)"
>
<template #modal-title>{{ $options.i18n.deleteModalTitle }}</template>
<gl-sprintf :message="$options.i18n.deleteModalContent">
<template #version>
......@@ -221,23 +264,22 @@ export default {
<strong>{{ packageEntity.name }}</strong>
</template>
</gl-sprintf>
</gl-modal>
<template #modal-footer>
<div class="gl-w-full">
<div class="float-right">
<gl-button @click="cancelDelete">{{ __('Cancel') }}</gl-button>
<gl-button
ref="modal-delete-button"
variant="danger"
category="primary"
data-qa-selector="delete_modal_button"
@click="confirmPackageDeletion"
>
{{ __('Delete') }}
</gl-button>
</div>
</div>
</template>
<gl-modal
ref="deleteFileModal"
modal-id="delete-file-modal"
:action-primary="$options.modal.fileDeletePrimaryAction"
:action-cancel="$options.modal.cancelAction"
@primary="confirmFileDelete"
@canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)"
>
<template #modal-title>{{ $options.i18n.deleteFileModalTitle }}</template>
<gl-sprintf v-if="fileToDelete" :message="$options.i18n.deleteFileModalContent">
<template #filename>
<strong>{{ fileToDelete.file_name }}</strong>
</template>
</gl-sprintf>
</gl-modal>
</div>
</template>
<script>
import { GlLink, GlTable } from '@gitlab/ui';
import { GlLink, GlTable, GlDropdownItem, GlDropdown, GlIcon } from '@gitlab/ui';
import { last } from 'lodash';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
......@@ -12,6 +12,9 @@ export default {
components: {
GlLink,
GlTable,
GlIcon,
GlDropdown,
GlDropdownItem,
FileIcon,
TimeAgoTooltip,
},
......@@ -22,6 +25,11 @@ export default {
required: false,
default: () => [],
},
canDelete: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
filesTableRows() {
......@@ -39,7 +47,6 @@ export default {
{
key: 'name',
label: __('Name'),
tdClass: 'gl-display-flex gl-align-items-center',
},
{
key: 'commit',
......@@ -55,6 +62,13 @@ export default {
label: __('Created'),
class: 'gl-text-right',
},
{
key: 'actions',
label: '',
hide: !this.canDelete,
class: 'gl-text-right',
tdClass: 'gl-w-4',
},
].filter((c) => !c.hide);
},
},
......@@ -63,6 +77,9 @@ export default {
return numberToHumanSize(size);
},
},
i18n: {
deleteFile: __('Delete file'),
},
};
</script>
......@@ -77,7 +94,7 @@ export default {
<template #cell(name)="{ item }">
<gl-link
:href="item.download_path"
class="gl-relative gl-text-gray-500"
class="gl-text-gray-500"
data-testid="download-link"
@click="$emit('download-file')"
>
......@@ -86,7 +103,7 @@ export default {
css-classes="gl-relative file-icon"
class="gl-mr-1 gl-relative"
/>
<span class="gl-relative">{{ item.file_name }}</span>
<span>{{ item.file_name }}</span>
</gl-link>
</template>
......@@ -103,6 +120,17 @@ export default {
<template #cell(created)="{ item }">
<time-ago-tooltip :time="item.created_at" />
</template>
<template #cell(actions)="{ item }">
<gl-dropdown category="tertiary" right>
<template #button-content>
<gl-icon name="ellipsis_v" />
</template>
<gl-dropdown-item data-testid="delete-file" @click="$emit('delete-file', item)">
{{ $options.i18n.deleteFile }}
</gl-dropdown-item>
</gl-dropdown>
</template>
</gl-table>
</div>
</template>
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
import createFlash from '~/flash';
import {
DELETE_PACKAGE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
} from '~/packages/shared/constants';
import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants';
import * as types from './mutation_types';
......@@ -16,7 +20,7 @@ export const fetchPackageVersions = ({ commit, state }) => {
}
})
.catch(() => {
createFlash(FETCH_PACKAGE_VERSIONS_ERROR);
createFlash({ message: FETCH_PACKAGE_VERSIONS_ERROR, type: 'warning' });
})
.finally(() => {
commit(types.SET_LOADING, false);
......@@ -29,6 +33,27 @@ export const deletePackage = ({
},
}) => {
return Api.deleteProjectPackage(project_id, id).catch(() => {
createFlash(DELETE_PACKAGE_ERROR_MESSAGE);
createFlash({ message: DELETE_PACKAGE_ERROR_MESSAGE, type: 'warning' });
});
};
export const deletePackageFile = (
{
state: {
packageEntity: { project_id, id },
packageFiles,
},
commit,
},
fileId,
) => {
return Api.deleteProjectPackageFile(project_id, id, fileId)
.then(() => {
const filtered = packageFiles.filter((f) => f.id !== fileId);
commit(types.UPDATE_PACKAGE_FILES, filtered);
createFlash({ message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, type: 'success' });
})
.catch(() => {
createFlash({ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, type: 'warning' });
});
};
export const SET_LOADING = 'SET_LOADING';
export const SET_PACKAGE_VERSIONS = 'SET_PACKAGE_VERSIONS';
export const UPDATE_PACKAGE_FILES = 'UPDATE_PACKAGE_FILES';
......@@ -11,4 +11,7 @@ export default {
versions,
};
},
[types.UPDATE_PACKAGE_FILES](state, files) {
state.packageFiles = files;
},
};
import { __ } from '~/locale';
import { __, s__ } from '~/locale';
export const PackageType = {
CONAN: 'conan',
......@@ -16,6 +16,9 @@ export const TrackingActions = {
REQUEST_DELETE_PACKAGE: 'request_delete_package',
CANCEL_DELETE_PACKAGE: 'cancel_delete_package',
PULL_PACKAGE: 'pull_package',
DELETE_PACKAGE_FILE: 'delete_package_file',
REQUEST_DELETE_PACKAGE_FILE: 'request_delete_package_file',
CANCEL_DELETE_PACKAGE_FILE: 'cancel_delete_package_file',
};
export const TrackingCategories = {
......@@ -25,7 +28,15 @@ export const TrackingCategories = {
};
export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
export const DELETE_PACKAGE_ERROR_MESSAGE = __('Something went wrong while deleting the package.');
export const DELETE_PACKAGE_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting the package.',
);
export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
__('PackageRegistry|Something went wrong while deleting the package file.'),
);
export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__(
'PackageRegistry|Package file deleted successfully',
);
export const PACKAGE_ERROR_STATUS = 'error';
export const PACKAGE_DEFAULT_STATUS = 'default';
......
......@@ -46,8 +46,8 @@ module Packages
size: package_file.size,
file_md5: package_file.file_md5,
file_sha1: package_file.file_sha1,
file_sha256: package_file.file_sha256
file_sha256: package_file.file_sha256,
id: package_file.id
}
file_view[:pipelines] = build_pipeline_infos(package_file.pipelines) if package_file.pipelines.present?
......
......@@ -10620,6 +10620,9 @@ msgstr ""
msgid "Delete domain"
msgstr ""
msgid "Delete file"
msgstr ""
msgid "Delete image repository"
msgstr ""
......@@ -23552,6 +23555,9 @@ msgstr ""
msgid "PackageRegistry|Created by commit %{link} on branch %{branch}"
msgstr ""
msgid "PackageRegistry|Delete Package File"
msgstr ""
msgid "PackageRegistry|Delete Package Version"
msgstr ""
......@@ -23630,6 +23636,9 @@ msgstr ""
msgid "PackageRegistry|Package Registry"
msgstr ""
msgid "PackageRegistry|Package file deleted successfully"
msgstr ""
msgid "PackageRegistry|Package has %{number} archived update"
msgstr ""
......@@ -23693,6 +23702,12 @@ msgstr ""
msgid "PackageRegistry|Show Yarn commands"
msgstr ""
msgid "PackageRegistry|Something went wrong while deleting the package file."
msgstr ""
msgid "PackageRegistry|Something went wrong while deleting the package."
msgstr ""
msgid "PackageRegistry|Sorry, your filter produced no results"
msgstr ""
......@@ -23723,6 +23738,9 @@ msgstr ""
msgid "PackageRegistry|Unable to load package"
msgstr ""
msgid "PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?"
msgstr ""
msgid "PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?"
msgstr ""
......@@ -30486,9 +30504,6 @@ msgstr ""
msgid "Something went wrong while deleting description changes. Please try again."
msgstr ""
msgid "Something went wrong while deleting the package."
msgstr ""
msgid "Something went wrong while deleting the source branch. Please try again."
msgstr ""
......
......@@ -116,6 +116,24 @@ describe('Api', () => {
});
});
});
describe('deleteProjectPackageFile', () => {
const packageFileId = 'package_file_id';
it('delete a package', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/packages/${packageId}/package_files/${packageFileId}`;
jest.spyOn(axios, 'delete');
mock.onDelete(expectedUrl).replyOnce(httpStatus.OK, true);
return Api.deleteProjectPackageFile(projectId, packageId, packageFileId).then(
({ data }) => {
expect(data).toEqual(true);
expect(axios.delete).toHaveBeenCalledWith(expectedUrl);
},
);
});
});
});
describe('container registry', () => {
......
import { GlEmptyState, GlModal } from '@gitlab/ui';
import { GlEmptyState } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import stubChildren from 'helpers/stub_children';
......@@ -34,6 +34,7 @@ describe('PackagesApp', () => {
let store;
const fetchPackageVersions = jest.fn();
const deletePackage = jest.fn();
const deletePackageFile = jest.fn();
const defaultProjectName = 'bar';
const { location } = window;
......@@ -59,6 +60,7 @@ describe('PackagesApp', () => {
actions: {
deletePackage,
fetchPackageVersions,
deletePackageFile,
},
getters,
});
......@@ -82,8 +84,8 @@ describe('PackagesApp', () => {
const packageTitle = () => wrapper.find(PackageTitle);
const emptyState = () => wrapper.find(GlEmptyState);
const deleteButton = () => wrapper.find('.js-delete-button');
const deleteModal = () => wrapper.find(GlModal);
const modalDeleteButton = () => wrapper.find({ ref: 'modal-delete-button' });
const findDeleteModal = () => wrapper.find({ ref: 'deleteModal' });
const findDeleteFileModal = () => wrapper.find({ ref: 'deleteFileModal' });
const versionsTab = () => wrapper.find('.js-versions-tab > a');
const packagesLoader = () => wrapper.find(PackagesListLoader);
const packagesVersionRows = () => wrapper.findAll(PackageListRow);
......@@ -110,7 +112,7 @@ describe('PackagesApp', () => {
it('renders the app and displays the package title', () => {
createComponent();
expect(packageTitle()).toExist();
expect(packageTitle().exists()).toBe(true);
});
it('renders an empty state component when no an invalid package is passed as a prop', () => {
......@@ -118,7 +120,7 @@ describe('PackagesApp', () => {
packageEntity: {},
});
expect(emptyState()).toExist();
expect(emptyState().exists()).toBe(true);
});
it('package history has the right props', () => {
......@@ -152,7 +154,16 @@ describe('PackagesApp', () => {
});
it('shows the delete confirmation modal when delete is clicked', () => {
expect(deleteModal()).toExist();
expect(findDeleteModal().exists()).toBe(true);
});
});
describe('deleting package files', () => {
it('shows the delete confirmation modal when delete is clicked', () => {
createComponent();
findPackageFiles().vm.$emit('delete-file', mavenFiles[0]);
expect(findDeleteFileModal().exists()).toBe(true);
});
});
......@@ -228,13 +239,7 @@ describe('PackagesApp', () => {
});
describe('tracking and delete', () => {
const doDelete = async () => {
deleteButton().trigger('click');
await wrapper.vm.$nextTick();
modalDeleteButton().trigger('click');
};
describe('delete', () => {
describe('delete package', () => {
const originalReferrer = document.referrer;
const setReferrer = (value = defaultProjectName) => {
Object.defineProperty(document, 'referrer', {
......@@ -250,9 +255,9 @@ describe('PackagesApp', () => {
});
});
it('calls the proper vuex action', async () => {
it('calls the proper vuex action', () => {
createComponent({ packageEntity: npmPackage });
await doDelete();
findDeleteModal().vm.$emit('primary');
expect(deletePackage).toHaveBeenCalled();
});
......@@ -260,7 +265,7 @@ describe('PackagesApp', () => {
setReferrer();
deletePackage.mockResolvedValue();
createComponent({ packageEntity: npmPackage });
await doDelete();
findDeleteModal().vm.$emit('primary');
await deletePackage();
expect(window.location.replace).toHaveBeenCalledWith(
'project_url?showSuccessDeleteAlert=true',
......@@ -271,7 +276,7 @@ describe('PackagesApp', () => {
setReferrer('baz');
deletePackage.mockResolvedValue();
createComponent({ packageEntity: npmPackage });
await doDelete();
findDeleteModal().vm.$emit('primary');
await deletePackage();
expect(window.location.replace).toHaveBeenCalledWith(
'group_url?showSuccessDeleteAlert=true',
......@@ -279,6 +284,17 @@ describe('PackagesApp', () => {
});
});
describe('delete file', () => {
it('calls the proper vuex action', () => {
createComponent({ packageEntity: npmPackage });
findPackageFiles().vm.$emit('delete-file', mavenFiles[0]);
findDeleteFileModal().vm.$emit('primary');
expect(deletePackageFile).toHaveBeenCalled();
});
});
describe('tracking', () => {
let eventSpy;
let utilSpy;
......@@ -295,9 +311,9 @@ describe('PackagesApp', () => {
expect(utilSpy).toHaveBeenCalledWith('conan');
});
it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, async () => {
it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, () => {
createComponent({ packageEntity: npmPackage });
await doDelete();
findDeleteModal().vm.$emit('primary');
expect(eventSpy).toHaveBeenCalledWith(
category,
TrackingActions.DELETE_PACKAGE,
......@@ -305,6 +321,56 @@ describe('PackagesApp', () => {
);
});
it(`canceling a package deletion tracks ${TrackingActions.CANCEL_DELETE_PACKAGE}`, () => {
createComponent({ packageEntity: npmPackage });
findDeleteModal().vm.$emit('canceled');
expect(eventSpy).toHaveBeenCalledWith(
category,
TrackingActions.CANCEL_DELETE_PACKAGE,
expect.any(Object),
);
});
it(`request a file deletion tracks ${TrackingActions.REQUEST_DELETE_PACKAGE_FILE}`, () => {
createComponent({ packageEntity: npmPackage });
findPackageFiles().vm.$emit('delete-file', mavenFiles[0]);
expect(eventSpy).toHaveBeenCalledWith(
category,
TrackingActions.REQUEST_DELETE_PACKAGE_FILE,
expect.any(Object),
);
});
it(`confirming a file deletion tracks ${TrackingActions.DELETE_PACKAGE_FILE}`, () => {
createComponent({ packageEntity: npmPackage });
findPackageFiles().vm.$emit('delete-file', npmPackage);
findDeleteFileModal().vm.$emit('primary');
expect(eventSpy).toHaveBeenCalledWith(
category,
TrackingActions.REQUEST_DELETE_PACKAGE_FILE,
expect.any(Object),
);
});
it(`canceling a file deletion tracks ${TrackingActions.CANCEL_DELETE_PACKAGE_FILE}`, () => {
createComponent({ packageEntity: npmPackage });
findPackageFiles().vm.$emit('delete-file', npmPackage);
findDeleteFileModal().vm.$emit('canceled');
expect(eventSpy).toHaveBeenCalledWith(
category,
TrackingActions.CANCEL_DELETE_PACKAGE_FILE,
expect.any(Object),
);
});
it(`file download link call event with ${TrackingActions.PULL_PACKAGE}`, () => {
createComponent({ packageEntity: conanPackage });
......
import { GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import stubChildren from 'helpers/stub_children';
import component from '~/packages/details/components/package_files.vue';
......@@ -12,16 +13,19 @@ describe('Package Files', () => {
const findAllRows = () => wrapper.findAll('[data-testid="file-row"');
const findFirstRow = () => findAllRows().at(0);
const findSecondRow = () => findAllRows().at(1);
const findFirstRowDownloadLink = () => findFirstRow().find('[data-testid="download-link"');
const findFirstRowCommitLink = () => findFirstRow().find('[data-testid="commit-link"');
const findSecondRowCommitLink = () => findSecondRow().find('[data-testid="commit-link"');
const findFirstRowDownloadLink = () => findFirstRow().find('[data-testid="download-link"]');
const findFirstRowCommitLink = () => findFirstRow().find('[data-testid="commit-link"]');
const findSecondRowCommitLink = () => findSecondRow().find('[data-testid="commit-link"]');
const findFirstRowFileIcon = () => findFirstRow().find(FileIcon);
const findFirstRowCreatedAt = () => findFirstRow().find(TimeAgoTooltip);
const findFirstActionMenu = () => findFirstRow().findComponent(GlDropdown);
const findActionMenuDelete = () => findFirstActionMenu().find('[data-testid="delete-file"]');
const createComponent = (packageFiles = npmFiles) => {
const createComponent = ({ packageFiles = npmFiles, canDelete = true } = {}) => {
wrapper = mount(component, {
propsData: {
packageFiles,
canDelete,
},
stubs: {
...stubChildren(component),
......@@ -43,7 +47,7 @@ describe('Package Files', () => {
});
it('renders multiple files for a package that contains more than one file', () => {
createComponent(mavenFiles);
createComponent({ packageFiles: mavenFiles });
expect(findAllRows()).toHaveLength(2);
});
......@@ -123,7 +127,7 @@ describe('Package Files', () => {
});
describe('when package file has no pipeline associated', () => {
it('does not exist', () => {
createComponent(mavenFiles);
createComponent({ packageFiles: mavenFiles });
expect(findFirstRowCommitLink().exists()).toBe(false);
});
......@@ -131,11 +135,50 @@ describe('Package Files', () => {
describe('when only one file lacks an associated pipeline', () => {
it('renders the commit when it exists and not otherwise', () => {
createComponent([npmFiles[0], mavenFiles[0]]);
createComponent({ packageFiles: [npmFiles[0], mavenFiles[0]] });
expect(findFirstRowCommitLink().exists()).toBe(true);
expect(findSecondRowCommitLink().exists()).toBe(false);
});
});
describe('action menu', () => {
describe('when the user can delete', () => {
it('exists', () => {
createComponent();
expect(findFirstActionMenu().exists()).toBe(true);
});
describe('menu items', () => {
describe('delete file', () => {
it('exists', () => {
createComponent();
expect(findActionMenuDelete().exists()).toBe(true);
});
it('emits a delete event when clicked', () => {
createComponent();
findActionMenuDelete().vm.$emit('click');
const [[{ id }]] = wrapper.emitted('delete-file');
expect(id).toBe(npmFiles[0].id);
});
});
});
});
describe('when the user can not delete', () => {
const canDelete = false;
it('does not exist', () => {
createComponent({ canDelete });
expect(findFirstActionMenu().exists()).toBe(false);
});
});
});
});
});
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages/details/constants';
import { fetchPackageVersions, deletePackage } from '~/packages/details/store/actions';
import {
fetchPackageVersions,
deletePackage,
deletePackageFile,
} from '~/packages/details/store/actions';
import * as types from '~/packages/details/store/mutation_types';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
import {
DELETE_PACKAGE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
} from '~/packages/shared/constants';
import { npmPackage as packageEntity } from '../../mock_data';
jest.mock('~/flash.js');
......@@ -74,7 +82,10 @@ describe('Actions Package details store', () => {
packageEntity.project_id,
packageEntity.id,
);
expect(createFlash).toHaveBeenCalledWith(FETCH_PACKAGE_VERSIONS_ERROR);
expect(createFlash).toHaveBeenCalledWith({
message: FETCH_PACKAGE_VERSIONS_ERROR,
type: 'warning',
});
done();
},
);
......@@ -96,7 +107,48 @@ describe('Actions Package details store', () => {
Api.deleteProjectPackage = jest.fn().mockRejectedValue();
testAction(deletePackage, undefined, { packageEntity }, [], [], () => {
expect(createFlash).toHaveBeenCalledWith(DELETE_PACKAGE_ERROR_MESSAGE);
expect(createFlash).toHaveBeenCalledWith({
message: DELETE_PACKAGE_ERROR_MESSAGE,
type: 'warning',
});
done();
});
});
});
describe('deletePackageFile', () => {
const fileId = 'a_file_id';
it('should call Api.deleteProjectPackageFile and commit the right data', (done) => {
const packageFiles = [{ id: 'foo' }, { id: fileId }];
Api.deleteProjectPackageFile = jest.fn().mockResolvedValue();
testAction(
deletePackageFile,
fileId,
{ packageEntity, packageFiles },
[{ type: types.UPDATE_PACKAGE_FILES, payload: [{ id: 'foo' }] }],
[],
() => {
expect(Api.deleteProjectPackageFile).toHaveBeenCalledWith(
packageEntity.project_id,
packageEntity.id,
fileId,
);
expect(createFlash).toHaveBeenCalledWith({
message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
type: 'success',
});
done();
},
);
});
it('should create flash on API error', (done) => {
Api.deleteProjectPackageFile = jest.fn().mockRejectedValue();
testAction(deletePackageFile, fileId, { packageEntity }, [], [], () => {
expect(createFlash).toHaveBeenCalledWith({
message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
type: 'warning',
});
done();
});
});
......
......@@ -28,4 +28,13 @@ describe('Mutations package details Store', () => {
expect(mockState.packageEntity.versions).toEqual(fakeVersions);
});
});
describe('UPDATE_PACKAGE_FILES', () => {
it('should update the packageFiles', () => {
const files = [1, 2, 3];
mutations[types.UPDATE_PACKAGE_FILES](mockState, files);
expect(mockState.packageFiles).toEqual(files);
});
});
});
......@@ -19,7 +19,8 @@ RSpec.describe ::Packages::Detail::PackagePresenter do
size: file.size,
file_md5: file.file_md5,
file_sha1: file.file_sha1,
file_sha256: file.file_sha256
file_sha256: file.file_sha256,
id: file.id
}
end
end
......
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