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