diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-04 03:08:55 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-04 03:08:55 +0300 |
commit | ff71e5f91c447686ab7bec7407dba0d4738a8807 (patch) | |
tree | de5ab12000d8b49cc7054d57db008280151851c6 /app/assets/javascripts/packages_and_registries | |
parent | e643b1a37690e16a3683aea0a3369e9723e17a35 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/packages_and_registries')
7 files changed, 191 insertions, 34 deletions
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue index 9430f1f0a4c..b872294d2cf 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue @@ -1,14 +1,16 @@ <script> -import { GlLink, GlTableLite, GlDropdownItem, GlDropdown, GlButton } from '@gitlab/ui'; +import { GlLink, GlTable, GlDropdownItem, GlDropdown, GlButton, GlFormCheckbox } from '@gitlab/ui'; import { last } from 'lodash'; import { numberToHumanSize } from '~/lib/utils/number_utils'; -import { __ } from '~/locale'; +import { __, s__ } from '~/locale'; import FileSha from '~/packages_and_registries/package_registry/components/details/file_sha.vue'; import Tracking from '~/tracking'; import { packageTypeToTrackCategory } from '~/packages_and_registries/package_registry/utils'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { + REQUEST_DELETE_SELECTED_PACKAGE_FILE_TRACKING_ACTION, + SELECT_PACKAGE_FILE_TRACKING_ACTION, TRACKING_LABEL_PACKAGE_ASSET, TRACKING_ACTION_EXPAND_PACKAGE_ASSET, } from '~/packages_and_registries/package_registry/constants'; @@ -17,9 +19,10 @@ export default { name: 'PackageFiles', components: { GlLink, - GlTableLite, + GlTable, GlDropdown, GlDropdownItem, + GlFormCheckbox, GlButton, FileIcon, TimeAgoTooltip, @@ -32,13 +35,29 @@ export default { required: false, default: false, }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, packageFiles: { type: Array, required: false, default: () => [], }, }, + data() { + return { + selectedReferences: [], + }; + }, computed: { + areFilesSelected() { + return this.selectedReferences.length > 0; + }, + areAllFilesSelected() { + return this.packageFiles.every(this.isSelected); + }, filesTableRows() { return this.packageFiles.map((pf) => ({ ...pf, @@ -46,6 +65,9 @@ export default { pipeline: last(pf.pipelines), })); }, + hasSelectedSomeFiles() { + return this.areFilesSelected && !this.areAllFilesSelected; + }, showCommitColumn() { // note that this is always false for now since we do not return // pipelines associated to files for performance concerns @@ -54,6 +76,12 @@ export default { filesTableHeaderFields() { return [ { + key: 'checkbox', + label: __('Select all'), + class: 'gl-w-4', + hide: !this.canDelete, + }, + { key: 'name', label: __('Name'), }, @@ -98,22 +126,71 @@ export default { this.track(TRACKING_ACTION_EXPAND_PACKAGE_ASSET, { label: TRACKING_LABEL_PACKAGE_ASSET }); } }, + updateSelectedReferences(selection) { + this.track(SELECT_PACKAGE_FILE_TRACKING_ACTION); + this.selectedReferences = selection; + }, + isSelected(packageFile) { + return this.selectedReferences.find((reference) => reference.id === packageFile.id); + }, + handleFileDeleteSelected() { + this.track(REQUEST_DELETE_SELECTED_PACKAGE_FILE_TRACKING_ACTION); + this.$emit('delete-files', this.selectedReferences); + }, }, i18n: { deleteFile: __('Delete file'), + deleteSelected: s__('PackageRegistry|Delete selected'), moreActionsText: __('More actions'), }, }; </script> <template> - <div> - <h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3> - <gl-table-lite + <div class="gl-pt-6"> + <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between"> + <h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3> + <gl-button + v-if="canDelete" + :disabled="isLoading || !areFilesSelected" + category="secondary" + variant="danger" + data-testid="delete-selected" + @click="handleFileDeleteSelected" + > + {{ $options.i18n.deleteSelected }} + </gl-button> + </div> + <gl-table :fields="filesTableHeaderFields" :items="filesTableRows" + show-empty + selectable + select-mode="multi" + selected-variant="primary" :tbody-tr-attr="{ 'data-testid': 'file-row' }" + @row-selected="updateSelectedReferences" > + <template #head(checkbox)="{ selectAllRows, clearSelected }"> + <gl-form-checkbox + v-if="canDelete" + data-testid="package-files-checkbox-all" + :checked="areAllFilesSelected" + :indeterminate="hasSelectedSomeFiles" + @change="areAllFilesSelected ? clearSelected() : selectAllRows()" + /> + </template> + + <template #cell(checkbox)="{ rowSelected, selectRow, unselectRow }"> + <gl-form-checkbox + v-if="canDelete" + class="gl-mt-1" + :checked="rowSelected" + data-testid="package-files-checkbox" + @change="rowSelected ? unselectRow() : selectRow()" + /> + </template> + <template #cell(name)="{ item, toggleDetails, detailsShowing }"> <gl-button v-if="hasDetails(item)" @@ -164,7 +241,7 @@ export default { no-caret right > - <gl-dropdown-item data-testid="delete-file" @click="$emit('delete-file', item)"> + <gl-dropdown-item data-testid="delete-file" @click="$emit('delete-files', [item])"> {{ $options.i18n.deleteFile }} </gl-dropdown-item> </gl-dropdown> @@ -184,6 +261,6 @@ export default { <file-sha v-if="item.fileSha1" data-testid="sha-1" title="SHA-1" :sha="item.fileSha1" /> </div> </template> - </gl-table-lite> + </gl-table> </div> </template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js index ad9be5137a2..5b2a347a4ee 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js +++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js @@ -7,9 +7,12 @@ export { CANCEL_DELETE_PACKAGE_TRACKING_ACTION, PULL_PACKAGE_TRACKING_ACTION, DELETE_PACKAGE_FILE_TRACKING_ACTION, + DELETE_PACKAGE_FILES_TRACKING_ACTION, REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION, + REQUEST_DELETE_SELECTED_PACKAGE_FILE_TRACKING_ACTION, CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION, DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION, + SELECT_PACKAGE_FILE_TRACKING_ACTION, } from '~/packages_and_registries/shared/constants'; export const PACKAGE_TYPE_CONAN = 'CONAN'; @@ -81,6 +84,12 @@ export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__( export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__( 'PackageRegistry|Package file deleted successfully', ); +export const DELETE_PACKAGE_FILES_ERROR_MESSAGE = s__( + 'PackageRegistry|Something went wrong while deleting the package assets.', +); +export const DELETE_PACKAGE_FILES_SUCCESS_MESSAGE = s__( + 'PackageRegistry|Package assets deleted successfully', +); export const FETCH_PACKAGE_DETAILS_ERROR_MESSAGE = s__( 'PackageRegistry|Failed to load the package data', ); diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql deleted file mode 100644 index f016640f57d..00000000000 --- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql +++ /dev/null @@ -1,5 +0,0 @@ -mutation destroyPackageFile($id: PackagesPackageFileID!) { - destroyPackageFile(input: { id: $id }) { - errors - } -} diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql new file mode 100644 index 00000000000..8f9a3156492 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql @@ -0,0 +1,5 @@ +mutation destroyPackageFiles($projectPath: ID!, $ids: [PackagesPackageFileID!]!) { + destroyPackageFiles(input: { projectPath: $projectPath, ids: $ids }) { + errors + } +} diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql index 5574020c9e4..f3f0d096d10 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql @@ -20,6 +20,7 @@ query getPackageDetails($id: PackagesPackageID!) { id path name + fullPath } tags(first: 10) { nodes { @@ -39,6 +40,9 @@ query getPackageDetails($id: PackagesPackageID!) { } } packageFiles(first: 100) { + pageInfo { + hasNextPage + } nodes { id fileMd5 diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue index bae6a510993..e83962bb608 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue @@ -34,16 +34,19 @@ import { REQUEST_DELETE_PACKAGE_TRACKING_ACTION, CANCEL_DELETE_PACKAGE_TRACKING_ACTION, DELETE_PACKAGE_FILE_TRACKING_ACTION, + DELETE_PACKAGE_FILES_TRACKING_ACTION, REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION, CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION, SHOW_DELETE_SUCCESS_ALERT, FETCH_PACKAGE_DETAILS_ERROR_MESSAGE, DELETE_PACKAGE_FILE_ERROR_MESSAGE, DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, + DELETE_PACKAGE_FILES_ERROR_MESSAGE, + DELETE_PACKAGE_FILES_SUCCESS_MESSAGE, DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION, } from '~/packages_and_registries/package_registry/constants'; -import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql'; +import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql'; import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql'; import Tracking from '~/tracking'; @@ -83,7 +86,8 @@ export default { }, data() { return { - fileToDelete: null, + filesToDelete: [], + mutationLoading: false, packageEntity: {}, }; }, @@ -114,6 +118,9 @@ export default { projectName() { return this.packageEntity.project?.name; }, + projectPath() { + return this.packageEntity.project?.fullPath; + }, packageId() { return this.$route.params.id; }, @@ -131,6 +138,9 @@ export default { isLoading() { return this.$apollo.queries.packageEntity.loading; }, + packageFilesLoading() { + return this.isLoading || this.mutationLoading; + }, isValidPackage() { return this.isLoading || Boolean(this.packageEntity.name); }, @@ -175,12 +185,14 @@ export default { window.location.replace(`${returnTo}?${modalQuery}`); }, - async deletePackageFile(id) { + async deletePackageFiles(ids) { + this.mutationLoading = true; try { const { data } = await this.$apollo.mutate({ - mutation: destroyPackageFileMutation, + mutation: destroyPackageFilesMutation, variables: { - id, + projectPath: this.projectPath, + ids, }, awaitRefetchQueries: true, refetchQueries: [ @@ -190,35 +202,53 @@ export default { }, ], }); - if (data?.destroyPackageFile?.errors[0]) { - throw data.destroyPackageFile.errors[0]; + if (data?.destroyPackageFiles?.errors[0]) { + throw data.destroyPackageFiles.errors[0]; } createFlash({ - message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, + message: + ids.length === 1 + ? DELETE_PACKAGE_FILE_SUCCESS_MESSAGE + : DELETE_PACKAGE_FILES_SUCCESS_MESSAGE, type: 'success', }); } catch (error) { createFlash({ - message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, + message: + ids.length === 1 + ? DELETE_PACKAGE_FILE_ERROR_MESSAGE + : DELETE_PACKAGE_FILES_ERROR_MESSAGE, type: 'warning', captureError: true, error, }); } + this.mutationLoading = false; }, - handleFileDelete(file) { + handleFileDelete(files) { this.track(REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION); - if (this.packageFiles.length === 1) { + if ( + files.length === this.packageFiles.length && + !this.packageEntity.packageFiles?.pageInfo?.hasNextPage + ) { this.$refs.deleteModal.show(); } else { - this.fileToDelete = { ...file }; - this.$refs.deleteFileModal.show(); + this.filesToDelete = files; + if (files.length === 1) { + this.$refs.deleteFileModal.show(); + } else if (files.length > 1) { + this.$refs.deleteFilesModal.show(); + } } }, - confirmFileDelete() { - this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION); - this.deletePackageFile(this.fileToDelete.id); - this.fileToDelete = null; + confirmFilesDelete() { + if (this.filesToDelete.length === 1) { + this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION); + } else { + this.track(DELETE_PACKAGE_FILES_TRACKING_ACTION); + } + this.deletePackageFiles(this.filesToDelete.map((file) => file.id)); + this.filesToDelete = []; }, }, i18n: { @@ -244,6 +274,10 @@ export default { text: __('Delete'), attributes: [{ variant: 'danger' }, { category: 'primary' }], }, + filesDeletePrimaryAction: { + text: s__('PackageRegistry|Permanently delete assets'), + attributes: [{ variant: 'danger' }, { category: 'primary' }], + }, cancelAction: { text: __('Cancel'), }, @@ -291,9 +325,10 @@ export default { <package-files v-if="showFiles" :can-delete="packageEntity.canDestroy" + :is-loading="packageFilesLoading" :package-files="packageFiles" @download-file="track($options.trackingActions.DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION)" - @delete-file="handleFileDelete" + @delete-files="handleFileDelete" /> </gl-tab> @@ -359,15 +394,43 @@ export default { :action-primary="$options.modal.fileDeletePrimaryAction" :action-cancel="$options.modal.cancelAction" data-testid="delete-file-modal" - @primary="confirmFileDelete" + @primary="confirmFilesDelete" @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)" > <template #modal-title>{{ $options.i18n.deleteFileModalTitle }}</template> - <gl-sprintf v-if="fileToDelete" :message="$options.i18n.deleteFileModalContent"> + <gl-sprintf v-if="filesToDelete.length === 1" :message="$options.i18n.deleteFileModalContent"> <template #filename> - <strong>{{ fileToDelete.file_name }}</strong> + <strong>{{ filesToDelete[0].fileName }}</strong> </template> </gl-sprintf> </gl-modal> + + <gl-modal + ref="deleteFilesModal" + size="sm" + modal-id="delete-files-modal" + :action-primary="$options.modal.filesDeletePrimaryAction" + :action-cancel="$options.modal.cancelAction" + data-testid="delete-files-modal" + @primary="confirmFilesDelete" + @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)" + > + <template #modal-title>{{ + n__( + `PackageRegistry|Delete 1 asset`, + `PackageRegistry|Delete %d assets`, + filesToDelete.length, + ) + }}</template> + <span v-if="filesToDelete.length > 0"> + {{ + n__( + `PackageRegistry|You are about to delete 1 asset. This operation is irreversible.`, + `PackageRegistry|You are about to delete %d assets. This operation is irreversible.`, + filesToDelete.length, + ) + }} + </span> + </gl-modal> </div> </template> diff --git a/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js b/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js index 5505205cf33..6744e821565 100644 --- a/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js +++ b/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js @@ -9,7 +9,11 @@ export const REQUEST_DELETE_PACKAGE_TRACKING_ACTION = 'request_delete_package'; export const CANCEL_DELETE_PACKAGE_TRACKING_ACTION = 'cancel_delete_package'; export const PULL_PACKAGE_TRACKING_ACTION = 'pull_package'; export const DELETE_PACKAGE_FILE_TRACKING_ACTION = 'delete_package_file'; +export const DELETE_PACKAGE_FILES_TRACKING_ACTION = 'delete_package_files'; +export const SELECT_PACKAGE_FILE_TRACKING_ACTION = 'select_package_file'; export const REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'request_delete_package_file'; +export const REQUEST_DELETE_SELECTED_PACKAGE_FILE_TRACKING_ACTION = + 'request_delete_selected_package_file'; export const CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'cancel_delete_package_file'; export const DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION = 'download_package_asset'; |