diff options
Diffstat (limited to 'app/assets/javascripts/repository')
15 files changed, 393 insertions, 33 deletions
diff --git a/app/assets/javascripts/repository/components/blob_replace.vue b/app/assets/javascripts/repository/components/blob_button_group.vue index 91d7811eb6d..273825b996a 100644 --- a/app/assets/javascripts/repository/components/blob_replace.vue +++ b/app/assets/javascripts/repository/components/blob_button_group.vue @@ -1,18 +1,22 @@ <script> -import { GlButton, GlModalDirective } from '@gitlab/ui'; +import { GlButtonGroup, GlButton, GlModalDirective } from '@gitlab/ui'; import { uniqueId } from 'lodash'; import { sprintf, __ } from '~/locale'; import getRefMixin from '../mixins/get_ref'; +import DeleteBlobModal from './delete_blob_modal.vue'; import UploadBlobModal from './upload_blob_modal.vue'; export default { i18n: { replace: __('Replace'), replacePrimaryBtnText: __('Replace file'), + delete: __('Delete'), }, components: { + GlButtonGroup, GlButton, UploadBlobModal, + DeleteBlobModal, }, directives: { GlModal: GlModalDirective, @@ -39,31 +43,50 @@ export default { type: String, required: true, }, + deletePath: { + type: String, + required: true, + }, canPushCode: { type: Boolean, required: true, }, + emptyRepo: { + type: Boolean, + required: true, + }, }, computed: { replaceModalId() { return uniqueId('replace-modal'); }, - title() { + replaceModalTitle() { return sprintf(__('Replace %{name}'), { name: this.name }); }, + deleteModalId() { + return uniqueId('delete-modal'); + }, + deleteModalTitle() { + return sprintf(__('Delete %{name}'), { name: this.name }); + }, }, }; </script> <template> <div class="gl-mr-3"> - <gl-button v-gl-modal="replaceModalId"> - {{ $options.i18n.replace }} - </gl-button> + <gl-button-group> + <gl-button v-gl-modal="replaceModalId"> + {{ $options.i18n.replace }} + </gl-button> + <gl-button v-gl-modal="deleteModalId"> + {{ $options.i18n.delete }} + </gl-button> + </gl-button-group> <upload-blob-modal :modal-id="replaceModalId" - :modal-title="title" - :commit-message="title" + :modal-title="replaceModalTitle" + :commit-message="replaceModalTitle" :target-branch="targetBranch || ref" :original-branch="originalBranch || ref" :can-push-code="canPushCode" @@ -71,5 +94,15 @@ export default { :replace-path="replacePath" :primary-btn-text="$options.i18n.replacePrimaryBtnText" /> + <delete-blob-modal + :modal-id="deleteModalId" + :modal-title="deleteModalTitle" + :delete-path="deletePath" + :commit-message="deleteModalTitle" + :target-branch="targetBranch || ref" + :original-branch="originalBranch || ref" + :can-push-code="canPushCode" + :empty-repo="emptyRepo" + /> </div> </template> diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index 7fbf331d585..09ac60c94c7 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -5,16 +5,19 @@ import BlobContent from '~/blob/components/blob_content.vue'; import BlobHeader from '~/blob/components/blob_header.vue'; import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants'; import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { isLoggedIn } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; import blobInfoQuery from '../queries/blob_info.query.graphql'; -import BlobHeaderEdit from './blob_header_edit.vue'; -import BlobReplace from './blob_replace.vue'; +import BlobButtonGroup from './blob_button_group.vue'; +import BlobEdit from './blob_edit.vue'; +import { loadViewer, viewerProps } from './blob_viewers'; export default { components: { BlobHeader, - BlobHeaderEdit, - BlobReplace, + BlobEdit, + BlobButtonGroup, BlobContent, GlLoadingIcon, }, @@ -31,9 +34,12 @@ export default { this.switchViewer( this.hasRichViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER, ); + if (this.hasRichViewer && !this.blobViewer) { + this.loadLegacyViewer(); + } }, error() { - createFlash({ message: __('An error occurred while loading the file. Please try again.') }); + this.displayError(); }, }, }, @@ -54,9 +60,16 @@ export default { }, data() { return { + legacyRichViewer: null, + isBinary: false, + isLoadingLegacyViewer: false, activeViewerType: SIMPLE_BLOB_VIEWER, project: { + userPermissions: { + pushCode: false, + }, repository: { + empty: true, blobs: { nodes: [ { @@ -77,10 +90,10 @@ export default { canLock: false, isLocked: false, lockLink: '', - canModifyBlob: true, forkPath: '', simpleViewer: {}, richViewer: null, + webPath: '', }, ], }, @@ -90,10 +103,10 @@ export default { }, computed: { isLoggedIn() { - return Boolean(gon.current_user_id); + return isLoggedIn(); }, isLoading() { - return this.$apollo.queries.project.loading; + return this.$apollo.queries.project.loading || this.isLoadingLegacyViewer; }, blobInfo() { const nodes = this.project?.repository?.blobs?.nodes; @@ -110,8 +123,30 @@ export default { hasRenderError() { return Boolean(this.viewer.renderError); }, + blobViewer() { + const { fileType } = this.viewer; + return loadViewer(fileType); + }, + viewerProps() { + const { fileType } = this.viewer; + return viewerProps(fileType, this.blobInfo); + }, }, methods: { + loadLegacyViewer() { + this.isLoadingLegacyViewer = true; + axios + .get(`${this.blobInfo.webPath}?format=json&viewer=rich`) + .then(({ data: { html, binary } }) => { + this.legacyRichViewer = html; + this.isBinary = binary; + this.isLoadingLegacyViewer = false; + }) + .catch(() => this.displayError()); + }, + displayError() { + createFlash({ message: __('An error occurred while loading the file. Please try again.') }); + }, switchViewer(newViewer) { this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER; }, @@ -121,36 +156,42 @@ export default { <template> <div> - <gl-loading-icon v-if="isLoading" /> + <gl-loading-icon v-if="isLoading" size="sm" /> <div v-if="blobInfo && !isLoading" class="file-holder"> <blob-header :blob="blobInfo" - :hide-viewer-switcher="!hasRichViewer" + :hide-viewer-switcher="!hasRichViewer || isBinary" :active-viewer-type="viewer.type" :has-render-error="hasRenderError" @viewer-changed="switchViewer" > <template #actions> - <blob-header-edit + <blob-edit + v-if="!isBinary" :edit-path="blobInfo.editBlobPath" :web-ide-path="blobInfo.ideEditPath" /> - <blob-replace + <blob-button-group v-if="isLoggedIn" :path="path" :name="blobInfo.name" :replace-path="blobInfo.replacePath" - :can-push-code="blobInfo.canModifyBlob" + :delete-path="blobInfo.webPath" + :can-push-code="project.userPermissions.pushCode" + :empty-repo="project.repository.empty" /> </template> </blob-header> <blob-content + v-if="!blobViewer" + :rich-viewer="legacyRichViewer" :blob="blobInfo" :content="blobInfo.rawTextBlob" :is-raw-content="true" :active-viewer="viewer" :loading="false" /> + <component :is="blobViewer" v-else v-bind="viewerProps" class="blob-viewer" /> </div> </div> </template> diff --git a/app/assets/javascripts/repository/components/blob_header_edit.vue b/app/assets/javascripts/repository/components/blob_edit.vue index 3d97ebe89e4..3d97ebe89e4 100644 --- a/app/assets/javascripts/repository/components/blob_header_edit.vue +++ b/app/assets/javascripts/repository/components/blob_edit.vue diff --git a/app/assets/javascripts/repository/components/blob_viewers/download_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/download_viewer.vue new file mode 100644 index 00000000000..48fa33eb558 --- /dev/null +++ b/app/assets/javascripts/repository/components/blob_viewers/download_viewer.vue @@ -0,0 +1,51 @@ +<script> +import { GlIcon, GlLink } from '@gitlab/ui'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import { sprintf, __ } from '~/locale'; + +export default { + components: { + GlIcon, + GlLink, + }, + props: { + fileName: { + type: String, + required: true, + }, + filePath: { + type: String, + required: true, + }, + fileSize: { + type: Number, + required: false, + default: 0, + }, + }, + computed: { + downloadFileSize() { + return numberToHumanSize(this.fileSize); + }, + downloadText() { + if (this.fileSize > 0) { + return sprintf(__('Download (%{fileSizeReadable})'), { + fileSizeReadable: this.downloadFileSize, + }); + } + return __('Download'); + }, + }, +}; +</script> + +<template> + <div class="gl-text-center gl-py-13 gl-bg-gray-50"> + <gl-link :href="filePath" rel="nofollow" :download="fileName" target="_blank"> + <div> + <gl-icon :size="16" name="download" class="gl-text-gray-900" /> + </div> + <h4>{{ downloadText }}</h4> + </gl-link> + </div> +</template> diff --git a/app/assets/javascripts/repository/components/blob_viewers/empty_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/empty_viewer.vue new file mode 100644 index 00000000000..53210cbcc93 --- /dev/null +++ b/app/assets/javascripts/repository/components/blob_viewers/empty_viewer.vue @@ -0,0 +1,3 @@ +<template> + <div class="nothing-here-block">{{ __('Empty file') }}</div> +</template> diff --git a/app/assets/javascripts/repository/components/blob_viewers/index.js b/app/assets/javascripts/repository/components/blob_viewers/index.js new file mode 100644 index 00000000000..4e16b16041f --- /dev/null +++ b/app/assets/javascripts/repository/components/blob_viewers/index.js @@ -0,0 +1,27 @@ +export const loadViewer = (type) => { + switch (type) { + case 'empty': + return () => import(/* webpackChunkName: 'blob_empty_viewer' */ './empty_viewer.vue'); + case 'text': + return () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue'); + case 'download': + return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue'); + default: + return null; + } +}; + +export const viewerProps = (type, blob) => { + return { + text: { + content: blob.rawTextBlob, + fileName: blob.name, + readOnly: true, + }, + download: { + fileName: blob.name, + filePath: blob.rawPath, + fileSize: blob.rawSize, + }, + }[type]; +}; diff --git a/app/assets/javascripts/repository/components/blob_viewers/text_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/text_viewer.vue new file mode 100644 index 00000000000..57fc979a56e --- /dev/null +++ b/app/assets/javascripts/repository/components/blob_viewers/text_viewer.vue @@ -0,0 +1,25 @@ +<script> +export default { + components: { + SourceEditor: () => + import(/* webpackChunkName: 'SourceEditor' */ '~/vue_shared/components/source_editor.vue'), + }, + props: { + content: { + type: String, + required: true, + }, + fileName: { + type: String, + required: true, + }, + readOnly: { + type: Boolean, + required: true, + }, + }, +}; +</script> +<template> + <source-editor :value="content" :file-name="fileName" :editor-options="{ readOnly }" /> +</template> diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue new file mode 100644 index 00000000000..6599d99d7bd --- /dev/null +++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue @@ -0,0 +1,151 @@ +<script> +import { GlModal, GlFormGroup, GlFormInput, GlFormTextarea, GlToggle } from '@gitlab/ui'; +import csrf from '~/lib/utils/csrf'; +import { __ } from '~/locale'; +import { + SECONDARY_OPTIONS_TEXT, + COMMIT_LABEL, + TARGET_BRANCH_LABEL, + TOGGLE_CREATE_MR_LABEL, +} from '../constants'; + +export default { + csrf, + components: { + GlModal, + GlFormGroup, + GlFormInput, + GlFormTextarea, + GlToggle, + }, + i18n: { + PRIMARY_OPTIONS_TEXT: __('Delete file'), + SECONDARY_OPTIONS_TEXT, + COMMIT_LABEL, + TARGET_BRANCH_LABEL, + TOGGLE_CREATE_MR_LABEL, + }, + props: { + modalId: { + type: String, + required: true, + }, + modalTitle: { + type: String, + required: true, + }, + deletePath: { + type: String, + required: true, + }, + commitMessage: { + type: String, + required: true, + }, + targetBranch: { + type: String, + required: true, + }, + originalBranch: { + type: String, + required: true, + }, + canPushCode: { + type: Boolean, + required: true, + }, + emptyRepo: { + type: Boolean, + required: true, + }, + }, + data() { + return { + loading: false, + commit: this.commitMessage, + target: this.targetBranch, + createNewMr: true, + error: '', + }; + }, + computed: { + primaryOptions() { + return { + text: this.$options.i18n.PRIMARY_OPTIONS_TEXT, + attributes: [ + { + variant: 'danger', + loading: this.loading, + disabled: !this.formCompleted || this.loading, + }, + ], + }; + }, + cancelOptions() { + return { + text: this.$options.i18n.SECONDARY_OPTIONS_TEXT, + attributes: [ + { + disabled: this.loading, + }, + ], + }; + }, + showCreateNewMrToggle() { + return this.canPushCode && this.target !== this.originalBranch; + }, + formCompleted() { + return this.commit && this.target; + }, + }, + methods: { + submitForm(e) { + e.preventDefault(); // Prevent modal from closing + this.loading = true; + this.$refs.form.submit(); + }, + }, +}; +</script> + +<template> + <gl-modal + :modal-id="modalId" + :title="modalTitle" + :action-primary="primaryOptions" + :action-cancel="cancelOptions" + @primary="submitForm" + > + <form ref="form" :action="deletePath" method="post"> + <input type="hidden" name="_method" value="delete" /> + <input :value="$options.csrf.token" type="hidden" name="authenticity_token" /> + <template v-if="emptyRepo"> + <!-- Once "empty_repo_upload_experiment" is made available, will need to add class 'js-branch-name' + Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/335721 --> + <input type="hidden" name="branch_name" :value="originalBranch" /> + </template> + <template v-else> + <input type="hidden" name="original_branch" :value="originalBranch" /> + <!-- Once "push to branch" permission is made available, will need to add to conditional + Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/335462 --> + <input v-if="createNewMr" type="hidden" name="create_merge_request" value="1" /> + <gl-form-group :label="$options.i18n.COMMIT_LABEL" label-for="commit_message"> + <gl-form-textarea v-model="commit" name="commit_message" :disabled="loading" /> + </gl-form-group> + <gl-form-group + v-if="canPushCode" + :label="$options.i18n.TARGET_BRANCH_LABEL" + label-for="branch_name" + > + <gl-form-input v-model="target" :disabled="loading" name="branch_name" /> + </gl-form-group> + <gl-toggle + v-if="showCreateNewMrToggle" + v-model="createNewMr" + :disabled="loading" + :label="$options.i18n.TOGGLE_CREATE_MR_LABEL" + /> + </template> + </form> + </gl-modal> +</template> diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index ca5711de49c..69eefc807d7 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -70,7 +70,7 @@ export default { ); }, showParentRow() { - return !this.isLoading && ['', '/'].indexOf(this.path) === -1; + return ['', '/'].indexOf(this.path) === -1; }, }, methods: { diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 62f863db871..82c18d13a6a 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -186,6 +186,8 @@ export default { :is="linkComponent" ref="link" v-gl-hover-load="handlePreload" + v-gl-tooltip:tooltip-container + :title="fullPath" :to="routerLinkTo" :href="url" :class="{ diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue index 794a8a85cc5..c861fb8dd06 100644 --- a/app/assets/javascripts/repository/components/tree_content.vue +++ b/app/assets/javascripts/repository/components/tree_content.vue @@ -1,8 +1,9 @@ <script> import filesQuery from 'shared_queries/repository/files.query.graphql'; import createFlash from '~/flash'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { __ } from '../../locale'; -import { TREE_PAGE_SIZE, TREE_INITIAL_FETCH_COUNT } from '../constants'; +import { TREE_PAGE_SIZE, TREE_INITIAL_FETCH_COUNT, TREE_PAGE_LIMIT } from '../constants'; import getRefMixin from '../mixins/get_ref'; import projectPathQuery from '../queries/project_path.query.graphql'; import { readmeFile } from '../utils/readme'; @@ -14,7 +15,7 @@ export default { FileTable, FilePreview, }, - mixins: [getRefMixin], + mixins: [getRefMixin, glFeatureFlagMixin()], apollo: { projectPath: { query: projectPathQuery, @@ -36,6 +37,7 @@ export default { return { projectPath: '', nextPageCursor: '', + pagesLoaded: 1, entries: { trees: [], submodules: [], @@ -44,16 +46,28 @@ export default { isLoadingFiles: false, isOverLimit: false, clickedShowMore: false, - pageSize: TREE_PAGE_SIZE, fetchCounter: 0, }; }, computed: { + pageSize() { + // we want to exponentially increase the page size to reduce the load on the frontend + const exponentialSize = (TREE_PAGE_SIZE / TREE_INITIAL_FETCH_COUNT) * (this.fetchCounter + 1); + return exponentialSize < TREE_PAGE_SIZE && this.glFeatures.increasePageSizeExponentially + ? exponentialSize + : TREE_PAGE_SIZE; + }, + totalEntries() { + return Object.values(this.entries).flat().length; + }, readme() { return readmeFile(this.entries.blobs); }, + pageLimitReached() { + return this.totalEntries / this.pagesLoaded >= TREE_PAGE_LIMIT; + }, hasShowMore() { - return !this.clickedShowMore && this.fetchCounter === TREE_INITIAL_FETCH_COUNT; + return !this.clickedShowMore && this.pageLimitReached; }, }, @@ -104,7 +118,7 @@ export default { if (pageInfo?.hasNextPage) { this.nextPageCursor = pageInfo.endCursor; this.fetchCounter += 1; - if (this.fetchCounter < TREE_INITIAL_FETCH_COUNT || this.clickedShowMore) { + if (!this.pageLimitReached || this.clickedShowMore) { this.fetchFiles(); this.clickedShowMore = false; } @@ -127,6 +141,7 @@ export default { }, handleShowMore() { this.clickedShowMore = true; + this.pagesLoaded += 1; this.fetchFiles(); }, }, diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue index 7f065dbdf6d..df5a5ea6163 100644 --- a/app/assets/javascripts/repository/components/upload_blob_modal.vue +++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue @@ -17,13 +17,15 @@ import { visitUrl, joinPaths } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; import { trackFileUploadEvent } from '~/projects/upload_file_experiment_tracking'; import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; +import { + SECONDARY_OPTIONS_TEXT, + COMMIT_LABEL, + TARGET_BRANCH_LABEL, + TOGGLE_CREATE_MR_LABEL, +} from '../constants'; const PRIMARY_OPTIONS_TEXT = __('Upload file'); -const SECONDARY_OPTIONS_TEXT = __('Cancel'); const MODAL_TITLE = __('Upload New File'); -const COMMIT_LABEL = __('Commit message'); -const TARGET_BRANCH_LABEL = __('Target branch'); -const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these changes'); const REMOVE_FILE_TEXT = __('Remove file'); const NEW_BRANCH_IN_FORK = __( 'A new branch will be created in your fork and a new merge request will be started.', @@ -170,7 +172,7 @@ export default { }) .catch(() => { this.loading = false; - createFlash(ERROR_MESSAGE); + createFlash({ message: ERROR_MESSAGE }); }); }, formData() { diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js index 62d5d3db445..2d2faa8d9f3 100644 --- a/app/assets/javascripts/repository/constants.js +++ b/app/assets/javascripts/repository/constants.js @@ -1,4 +1,10 @@ -const TREE_PAGE_LIMIT = 1000; // the maximum amount of items per page +import { __ } from '~/locale'; +export const TREE_PAGE_LIMIT = 1000; // the maximum amount of items per page export const TREE_PAGE_SIZE = 100; // the amount of items to be fetched per (batch) request export const TREE_INITIAL_FETCH_COUNT = TREE_PAGE_LIMIT / TREE_PAGE_SIZE; // the amount of (batch) requests to make + +export const SECONDARY_OPTIONS_TEXT = __('Cancel'); +export const COMMIT_LABEL = __('Commit message'); +export const TARGET_BRANCH_LABEL = __('Target branch'); +export const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these changes'); diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql index bfd9447d260..a8f263941e2 100644 --- a/app/assets/javascripts/repository/queries/blob_info.query.graphql +++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql @@ -1,6 +1,10 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) { project(fullPath: $projectPath) { + userPermissions { + pushCode + } repository { + empty blobs(paths: [$filePath]) { nodes { webPath @@ -15,7 +19,6 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) { storedExternally rawPath replacePath - canModifyBlob simpleViewer { fileType tooLarge diff --git a/app/assets/javascripts/repository/queries/commit.fragment.graphql b/app/assets/javascripts/repository/queries/commit.fragment.graphql index be6897b9a16..b046fc1f730 100644 --- a/app/assets/javascripts/repository/queries/commit.fragment.graphql +++ b/app/assets/javascripts/repository/queries/commit.fragment.graphql @@ -5,5 +5,6 @@ fragment TreeEntryCommit on LogTreeCommit { committedDate commitPath fileName + filePath type } |