diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 08:43:02 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 08:43:02 +0000 |
commit | d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch) | |
tree | 2341ef426af70ad1e289c38036737e04b0aa5007 /app/assets/javascripts/repository/components | |
parent | d6e514dd13db8947884cd58fe2a9c2a063400a9b (diff) | |
download | gitlab-ce-d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb.tar.gz |
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'app/assets/javascripts/repository/components')
12 files changed, 491 insertions, 35 deletions
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index 1d79818cbe8..7ad9fb56972 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -8,10 +8,12 @@ import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { isLoggedIn } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; +import { redirectTo } from '~/lib/utils/url_utility'; import getRefMixin from '../mixins/get_ref'; import blobInfoQuery from '../queries/blob_info.query.graphql'; import BlobButtonGroup from './blob_button_group.vue'; import BlobEdit from './blob_edit.vue'; +import ForkSuggestion from './fork_suggestion.vue'; import { loadViewer, viewerProps } from './blob_viewers'; export default { @@ -21,6 +23,7 @@ export default { BlobButtonGroup, BlobContent, GlLoadingIcon, + ForkSuggestion, }, mixins: [getRefMixin], inject: { @@ -42,9 +45,6 @@ export default { this.switchViewer( this.hasRichViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER, ); - if (this.hasRichViewer && !this.blobViewer) { - this.loadLegacyViewer(); - } }, error() { this.displayError(); @@ -68,7 +68,9 @@ export default { }, data() { return { + forkTarget: null, legacyRichViewer: null, + legacySimpleViewer: null, isBinary: false, isLoadingLegacyViewer: false, activeViewerType: SIMPLE_BLOB_VIEWER, @@ -76,6 +78,8 @@ export default { userPermissions: { pushCode: false, downloadCode: false, + createMergeRequestIn: false, + forkProject: false, }, pathLocks: { nodes: [], @@ -94,12 +98,14 @@ export default { path: '', editBlobPath: '', ideEditPath: '', + forkAndEditPath: '', + ideForkAndEditPath: '', storedExternally: false, + canModifyBlob: false, rawPath: '', externalStorageUrl: '', replacePath: '', deletePath: '', - forkPath: '', simpleViewer: {}, richViewer: null, webPath: '', @@ -115,7 +121,7 @@ export default { return isLoggedIn(); }, isLoading() { - return this.$apollo.queries.project.loading || this.isLoadingLegacyViewer; + return this.$apollo.queries.project.loading; }, isBinaryFileType() { return this.isBinary || this.blobInfo.simpleViewer?.fileType !== 'text'; @@ -151,24 +157,66 @@ export default { isLocked() { return this.project.pathLocks.nodes.some((node) => node.path === this.path); }, + showForkSuggestion() { + const { createMergeRequestIn, forkProject } = this.project.userPermissions; + const { canModifyBlob } = this.blobInfo; + + return this.isLoggedIn && !canModifyBlob && createMergeRequestIn && forkProject; + }, + forkPath() { + return this.forkTarget === 'ide' + ? this.blobInfo.ideForkAndEditPath + : this.blobInfo.forkAndEditPath; + }, }, methods: { - loadLegacyViewer() { + loadLegacyViewer(type) { + if (this.legacyViewerLoaded(type)) { + return; + } + this.isLoadingLegacyViewer = true; axios - .get(`${this.blobInfo.webPath}?format=json&viewer=rich`) + .get(`${this.blobInfo.webPath}?format=json&viewer=${type}`) .then(({ data: { html, binary } }) => { - this.legacyRichViewer = html; + if (type === 'simple') { + this.legacySimpleViewer = html; + } else { + this.legacyRichViewer = html; + } + this.isBinary = binary; this.isLoadingLegacyViewer = false; }) .catch(() => this.displayError()); }, + legacyViewerLoaded(type) { + return ( + (type === SIMPLE_BLOB_VIEWER && this.legacySimpleViewer) || + (type === RICH_BLOB_VIEWER && this.legacyRichViewer) + ); + }, displayError() { createFlash({ message: __('An error occurred while loading the file. Please try again.') }); }, switchViewer(newViewer) { this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER; + + if (!this.blobViewer) { + this.loadLegacyViewer(this.activeViewerType); + } + }, + editBlob(target) { + if (this.showForkSuggestion) { + this.setForkTarget(target); + return; + } + + const { ideEditPath, editBlobPath } = this.blobInfo; + redirectTo(target === 'ide' ? ideEditPath : editBlobPath); + }, + setForkTarget(target) { + this.forkTarget = target; }, }, }; @@ -191,6 +239,8 @@ export default { :show-edit-button="!isBinaryFileType" :edit-path="blobInfo.editBlobPath" :web-ide-path="blobInfo.ideEditPath" + :needs-to-fork="showForkSuggestion" + @edit="editBlob" /> <blob-button-group v-if="isLoggedIn" @@ -206,14 +256,20 @@ export default { /> </template> </blob-header> + <fork-suggestion + v-if="forkTarget && showForkSuggestion" + :fork-path="forkPath" + @cancel="setForkTarget(null)" + /> <blob-content v-if="!blobViewer" :rich-viewer="legacyRichViewer" :blob="blobInfo" - :content="blobInfo.rawTextBlob" + :content="legacySimpleViewer" :is-raw-content="true" :active-viewer="viewer" - :loading="false" + :hide-line-numbers="true" + :loading="isLoadingLegacyViewer" /> <component :is="blobViewer" v-else v-bind="viewerProps" class="blob-viewer" /> </div> diff --git a/app/assets/javascripts/repository/components/blob_edit.vue b/app/assets/javascripts/repository/components/blob_edit.vue index 30ed4cd57f1..fd377ba1b81 100644 --- a/app/assets/javascripts/repository/components/blob_edit.vue +++ b/app/assets/javascripts/repository/components/blob_edit.vue @@ -27,6 +27,16 @@ export default { type: String, required: true, }, + needsToFork: { + type: Boolean, + required: false, + default: false, + }, + }, + methods: { + onEdit(target) { + this.$emit('edit', target); + }, }, }; </script> @@ -38,7 +48,9 @@ export default { class="gl-mr-3" :edit-url="editPath" :web-ide-url="webIdePath" + :needs-to-fork="needsToFork" :is-blob="true" + @edit="onEdit" /> <div v-else> <gl-button @@ -46,8 +58,8 @@ export default { class="gl-mr-2" category="primary" variant="confirm" - :href="editPath" data-testid="edit" + @click="onEdit('simple')" > {{ $options.i18n.edit }} </gl-button> @@ -56,8 +68,8 @@ export default { class="gl-mr-3" category="primary" variant="confirm" - :href="webIdePath" data-testid="web-ide" + @click="onEdit('ide')" > {{ $options.i18n.webIde }} </gl-button> diff --git a/app/assets/javascripts/repository/components/blob_viewers/index.js b/app/assets/javascripts/repository/components/blob_viewers/index.js index 3b4f4eb51fe..c5209d97abb 100644 --- a/app/assets/javascripts/repository/components/blob_viewers/index.js +++ b/app/assets/javascripts/repository/components/blob_viewers/index.js @@ -3,11 +3,15 @@ export const loadViewer = (type) => { case 'empty': return () => import(/* webpackChunkName: 'blob_empty_viewer' */ './empty_viewer.vue'); case 'text': - return () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue'); + return gon.features.refactorTextViewer + ? () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue') + : null; case 'download': return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue'); case 'image': return () => import(/* webpackChunkName: 'blob_image_viewer' */ './image_viewer.vue'); + case 'video': + return () => import(/* webpackChunkName: 'blob_video_viewer' */ './video_viewer.vue'); default: return null; } @@ -29,5 +33,8 @@ export const viewerProps = (type, blob) => { url: blob.rawPath, alt: blob.name, }, + video: { + url: blob.rawPath, + }, }[type]; }; diff --git a/app/assets/javascripts/repository/components/blob_viewers/video_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/video_viewer.vue new file mode 100644 index 00000000000..dec0c4802ca --- /dev/null +++ b/app/assets/javascripts/repository/components/blob_viewers/video_viewer.vue @@ -0,0 +1,15 @@ +<script> +export default { + props: { + url: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div class="gl-text-center gl-p-7 gl-bg-gray-50"> + <video :src="url" controls data-testid="video" class="gl-max-w-full"></video> + </div> +</template> diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue index db84e2b5912..d3717f10ec7 100644 --- a/app/assets/javascripts/repository/components/breadcrumbs.vue +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -9,11 +9,13 @@ import { } from '@gitlab/ui'; import permissionsQuery from 'shared_queries/repository/permissions.query.graphql'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { __ } from '../../locale'; import getRefMixin from '../mixins/get_ref'; import projectPathQuery from '../queries/project_path.query.graphql'; import projectShortPathQuery from '../queries/project_short_path.query.graphql'; import UploadBlobModal from './upload_blob_modal.vue'; +import NewDirectoryModal from './new_directory_modal.vue'; const ROW_TYPES = { header: 'header', @@ -21,6 +23,7 @@ const ROW_TYPES = { }; const UPLOAD_BLOB_MODAL_ID = 'modal-upload-blob'; +const NEW_DIRECTORY_MODAL_ID = 'modal-new-directory'; export default { components: { @@ -30,6 +33,7 @@ export default { GlDropdownItem, GlIcon, UploadBlobModal, + NewDirectoryModal, }, apollo: { projectShortPath: { @@ -54,7 +58,7 @@ export default { directives: { GlModal: GlModalDirective, }, - mixins: [getRefMixin], + mixins: [getRefMixin, glFeatureFlagsMixin()], props: { currentPath: { type: String, @@ -121,8 +125,14 @@ export default { required: false, default: '', }, + newDirPath: { + type: String, + required: false, + default: '', + }, }, uploadBlobModalId: UPLOAD_BLOB_MODAL_ID, + newDirectoryModalId: NEW_DIRECTORY_MODAL_ID, data() { return { projectShortPath: '', @@ -160,6 +170,13 @@ export default { showUploadModal() { return this.canEditTree && !this.$apollo.queries.userPermissions.loading; }, + showNewDirectoryModal() { + return ( + this.glFeatures.newDirModal && + this.canEditTree && + !this.$apollo.queries.userPermissions.loading + ); + }, dropdownItems() { const items = []; @@ -185,15 +202,26 @@ export default { text: __('Upload file'), modalId: UPLOAD_BLOB_MODAL_ID, }, - { + ); + + if (this.glFeatures.newDirModal) { + items.push({ + attrs: { + href: '#modal-create-new-dir', + }, + text: __('New directory'), + modalId: NEW_DIRECTORY_MODAL_ID, + }); + } else { + items.push({ attrs: { href: '#modal-create-new-dir', 'data-target': '#modal-create-new-dir', 'data-toggle': 'modal', }, text: __('New directory'), - }, - ); + }); + } } else if (this.canCreateMrFromFork) { items.push( { @@ -306,5 +334,14 @@ export default { :can-push-code="canPushCode" :path="uploadPath" /> + <new-directory-modal + v-if="showNewDirectoryModal" + :can-push-code="canPushCode" + :modal-id="$options.newDirectoryModalId" + :commit-message="__('Add new directory')" + :target-branch="selectedBranch" + :original-branch="originalBranch" + :path="newDirPath" + /> </nav> </template> diff --git a/app/assets/javascripts/repository/components/fork_suggestion.vue b/app/assets/javascripts/repository/components/fork_suggestion.vue new file mode 100644 index 00000000000..c266bea319b --- /dev/null +++ b/app/assets/javascripts/repository/components/fork_suggestion.vue @@ -0,0 +1,45 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + i18n: { + message: __( + 'You can’t edit files directly in this project. Fork this project and submit a merge request with your changes.', + ), + fork: __('Fork'), + cancel: __('Cancel'), + }, + components: { + GlButton, + }, + props: { + forkPath: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <div + class="gl-display-flex gl-justify-content-end gl-align-items-center gl-bg-gray-10 gl-px-5 gl-py-2 gl-border-1 gl-border-b-solid gl-border-gray-100" + > + <span class="gl-mr-6" data-testid="message">{{ $options.i18n.message }}</span> + + <gl-button + class="gl-mr-3" + category="secondary" + variant="confirm" + :href="forkPath" + data-testid="fork" + > + {{ $options.i18n.fork }} + </gl-button> + + <gl-button data-testid="cancel" @click="$emit('cancel')"> + {{ $options.i18n.cancel }} + </gl-button> + </div> +</template> diff --git a/app/assets/javascripts/repository/components/new_directory_modal.vue b/app/assets/javascripts/repository/components/new_directory_modal.vue new file mode 100644 index 00000000000..6c5797bf5b2 --- /dev/null +++ b/app/assets/javascripts/repository/components/new_directory_modal.vue @@ -0,0 +1,183 @@ +<script> +import { + GlAlert, + GlForm, + GlModal, + GlFormGroup, + GlFormInput, + GlFormTextarea, + GlToggle, +} from '@gitlab/ui'; +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { visitUrl } from '~/lib/utils/url_utility'; +import { __ } from '~/locale'; +import { + SECONDARY_OPTIONS_TEXT, + COMMIT_LABEL, + TARGET_BRANCH_LABEL, + TOGGLE_CREATE_MR_LABEL, + NEW_BRANCH_IN_FORK, +} from '../constants'; + +const MODAL_TITLE = __('Create New Directory'); +const PRIMARY_OPTIONS_TEXT = __('Create directory'); +const DIR_LABEL = __('Directory name'); +const ERROR_MESSAGE = __('Error creating new directory. Please try again.'); + +export default { + components: { + GlAlert, + GlModal, + GlForm, + GlFormGroup, + GlFormInput, + GlFormTextarea, + GlToggle, + }, + i18n: { + DIR_LABEL, + COMMIT_LABEL, + TARGET_BRANCH_LABEL, + TOGGLE_CREATE_MR_LABEL, + NEW_BRANCH_IN_FORK, + PRIMARY_OPTIONS_TEXT, + ERROR_MESSAGE, + }, + props: { + modalTitle: { + type: String, + default: MODAL_TITLE, + required: false, + }, + modalId: { + type: String, + required: true, + }, + primaryBtnText: { + type: String, + default: PRIMARY_OPTIONS_TEXT, + required: false, + }, + commitMessage: { + type: String, + required: true, + }, + targetBranch: { + type: String, + required: true, + }, + originalBranch: { + type: String, + required: true, + }, + path: { + type: String, + required: true, + }, + canPushCode: { + type: Boolean, + required: true, + }, + }, + data() { + return { + dir: null, + commit: this.commitMessage, + target: this.targetBranch, + createNewMr: true, + loading: false, + }; + }, + computed: { + primaryOptions() { + return { + text: this.primaryBtnText, + attributes: [ + { + variant: 'confirm', + loading: this.loading, + disabled: !this.formCompleted || this.loading, + }, + ], + }; + }, + cancelOptions() { + return { + text: SECONDARY_OPTIONS_TEXT, + attributes: [ + { + disabled: this.loading, + }, + ], + }; + }, + showCreateNewMrToggle() { + return this.canPushCode; + }, + formCompleted() { + return this.dir && this.commit && this.target; + }, + }, + methods: { + submitForm() { + this.loading = true; + + const formData = new FormData(); + formData.append('dir_name', this.dir); + formData.append('commit_message', this.commit); + formData.append('branch_name', this.target); + formData.append('original_branch', this.originalBranch); + + if (this.createNewMr) { + formData.append('create_merge_request', this.createNewMr); + } + + return axios + .post(this.path, formData) + .then((response) => { + visitUrl(response.data.filePath); + }) + .catch(() => { + this.loading = false; + createFlash({ message: ERROR_MESSAGE }); + }); + }, + }, +}; +</script> + +<template> + <gl-form> + <gl-modal + :modal-id="modalId" + :title="modalTitle" + :action-primary="primaryOptions" + :action-cancel="cancelOptions" + @primary.prevent="submitForm" + > + <gl-form-group :label="$options.i18n.DIR_LABEL" label-for="dir_name"> + <gl-form-input v-model="dir" :disabled="loading" name="dir_name" /> + </gl-form-group> + <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" + /> + <gl-alert v-if="!canPushCode" variant="info" :dismissible="false" class="gl-mt-3"> + {{ $options.i18n.NEW_BRANCH_IN_FORK }} + </gl-alert> + </gl-modal> + </gl-form> +</template> diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue index 54e67c5ab5c..c6e461b10e0 100644 --- a/app/assets/javascripts/repository/components/preview/index.vue +++ b/app/assets/javascripts/repository/components/preview/index.vue @@ -1,5 +1,5 @@ <script> -import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui'; +import { GlIcon, GlLink, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; import { handleLocationHash } from '~/lib/utils/common_utils'; @@ -22,6 +22,9 @@ export default { GlLink, GlLoadingIcon, }, + directives: { + SafeHtml, + }, props: { blob: { type: Object, @@ -59,11 +62,7 @@ export default { </div> <div class="blob-viewer" data-qa-selector="blob_viewer_content" itemprop="about"> <gl-loading-icon v-if="loading > 0" size="md" color="dark" class="my-4 mx-auto" /> - <div - v-else-if="readme" - ref="readme" - v-html="readme.html /* eslint-disable-line vue/no-v-html */" - ></div> + <div v-else-if="readme" ref="readme" v-safe-html="readme.html"></div> </div> </article> </template> diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index 10a30bd44b1..0a2ed753e38 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -1,5 +1,6 @@ <script> import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlButton } from '@gitlab/ui'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { sprintf, __ } from '../../../locale'; import getRefMixin from '../../mixins/get_ref'; import projectPathQuery from '../../queries/project_path.query.graphql'; @@ -15,13 +16,18 @@ export default { ParentRow, GlButton, }, - mixins: [getRefMixin], + mixins: [getRefMixin, glFeatureFlagMixin()], apollo: { projectPath: { query: projectPathQuery, }, }, props: { + commits: { + type: Array, + required: false, + default: () => [], + }, path: { type: String, required: true, @@ -48,6 +54,7 @@ export default { data() { return { projectPath: '', + rowNumbers: {}, }; }, computed: { @@ -73,10 +80,38 @@ export default { return ['', '/'].indexOf(this.path) === -1; }, }, + watch: { + $route: function routeChange() { + this.$options.totalRowsLoaded = -1; + }, + }, + totalRowsLoaded: -1, methods: { showMore() { this.$emit('showMore'); }, + generateRowNumber(path, id, index) { + const key = `${path}-${id}-${index}`; + if (!this.glFeatures.lazyLoadCommits) { + return 0; + } + + if (!this.rowNumbers[key] && this.rowNumbers[key] !== 0) { + this.$options.totalRowsLoaded += 1; + this.rowNumbers[key] = this.$options.totalRowsLoaded; + } + + return this.rowNumbers[key]; + }, + getCommit(fileName, type) { + if (!this.glFeatures.lazyLoadCommits) { + return {}; + } + + return this.commits.find( + (commitEntry) => commitEntry.fileName === fileName && commitEntry.type === type, + ); + }, }, }; </script> @@ -87,6 +122,7 @@ export default { <table :aria-label="tableCaption" class="table tree-table" + :class="{ 'gl-table-layout-fixed': !showParentRow }" aria-live="polite" data-qa-selector="file_tree_table" > @@ -115,12 +151,17 @@ export default { :lfs-oid="entry.lfsOid" :loading-path="loadingPath" :total-entries="totalEntries" + :row-number="generateRowNumber(entry.flatPath, entry.id, index)" + :commit-info="getCommit(entry.name, entry.type)" + v-on="$listeners" /> </template> <template v-if="isLoading"> <tr v-for="i in 5" :key="i" aria-hidden="true"> <td><gl-skeleton-loading :lines="1" class="h-auto" /></td> - <td><gl-skeleton-loading :lines="1" class="h-auto" /></td> + <td class="gl-display-none gl-sm-display-block"> + <gl-skeleton-loading :lines="1" class="h-auto" /> + </td> <td><gl-skeleton-loading :lines="1" class="ml-auto h-auto w-50" /></td> </tr> </template> diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 009dd19b4a5..5010d60f374 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -7,6 +7,8 @@ import { GlLoadingIcon, GlIcon, GlHoverLoadDirective, + GlSafeHtmlDirective, + GlIntersectionObserver, } from '@gitlab/ui'; import { escapeRegExp } from 'lodash'; import filesQuery from 'shared_queries/repository/files.query.graphql'; @@ -29,10 +31,12 @@ export default { GlIcon, TimeagoTooltip, FileIcon, + GlIntersectionObserver, }, directives: { GlTooltip: GlTooltipDirective, GlHoverLoad: GlHoverLoadDirective, + SafeHtml: GlSafeHtmlDirective, }, apollo: { commit: { @@ -46,10 +50,23 @@ export default { maxOffset: this.totalEntries, }; }, + skip() { + return this.glFeatures.lazyLoadCommits; + }, }, }, mixins: [getRefMixin, glFeatureFlagMixin()], props: { + commitInfo: { + type: Object, + required: false, + default: null, + }, + rowNumber: { + type: Number, + required: false, + default: null, + }, totalEntries: { type: Number, required: true, @@ -111,9 +128,13 @@ export default { data() { return { commit: null, + hasRowAppeared: false, }; }, computed: { + commitData() { + return this.glFeatures.lazyLoadCommits ? this.commitInfo : this.commit; + }, refactorBlobViewerEnabled() { return this.glFeatures.refactorBlobViewer; }, @@ -146,7 +167,10 @@ export default { return this.sha.slice(0, 8); }, hasLockLabel() { - return this.commit && this.commit.lockLabel; + return this.commitData && this.commitData.lockLabel; + }, + showSkeletonLoader() { + return !this.commitData && this.hasRowAppeared; }, }, methods: { @@ -177,7 +201,21 @@ export default { apolloQuery(query, variables) { this.$apollo.query({ query, variables }); }, + rowAppeared() { + this.hasRowAppeared = true; + + if (this.glFeatures.lazyLoadCommits) { + this.$emit('row-appear', { + rowNumber: this.rowNumber, + hasCommit: Boolean(this.commitInfo), + }); + } + }, + rowDisappeared() { + this.hasRowAppeared = false; + }, }, + safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] }, }; </script> @@ -219,7 +257,7 @@ export default { <gl-icon v-if="hasLockLabel" v-gl-tooltip - :title="commit.lockLabel" + :title="commitData.lockLabel" name="lock" :size="12" class="ml-1" @@ -227,17 +265,19 @@ export default { </td> <td class="d-none d-sm-table-cell tree-commit cursor-default"> <gl-link - v-if="commit" - :href="commit.commitPath" - :title="commit.message" + v-if="commitData" + v-safe-html:[$options.safeHtmlConfig]="commitData.titleHtml" + :href="commitData.commitPath" + :title="commitData.message" class="str-truncated-100 tree-commit-link" - v-html="commit.titleHtml /* eslint-disable-line vue/no-v-html */" /> - <gl-skeleton-loading v-else :lines="1" class="h-auto" /> + <gl-intersection-observer @appear="rowAppeared" @disappear="rowDisappeared"> + <gl-skeleton-loading v-if="showSkeletonLoader" :lines="1" class="h-auto" /> + </gl-intersection-observer> </td> <td class="tree-time-ago text-right cursor-default"> - <timeago-tooltip v-if="commit" :time="commit.committedDate" /> - <gl-skeleton-loading v-else :lines="1" class="ml-auto h-auto w-50" /> + <timeago-tooltip v-if="commitData" :time="commitData.committedDate" /> + <gl-skeleton-loading v-if="showSkeletonLoader" :lines="1" class="ml-auto h-auto w-50" /> </td> </tr> </template> diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue index 5a8ead9ae8f..16dfe3cfb14 100644 --- a/app/assets/javascripts/repository/components/tree_content.vue +++ b/app/assets/javascripts/repository/components/tree_content.vue @@ -8,6 +8,7 @@ import { TREE_PAGE_SIZE, TREE_INITIAL_FETCH_COUNT, TREE_PAGE_LIMIT } from '../co import getRefMixin from '../mixins/get_ref'; import projectPathQuery from '../queries/project_path.query.graphql'; import { readmeFile } from '../utils/readme'; +import { loadCommits, isRequested, resetRequestedCommits } from '../commits_service'; import FilePreview from './preview/index.vue'; import FileTable from './table/index.vue'; @@ -36,6 +37,7 @@ export default { }, data() { return { + commits: [], projectPath: '', nextPageCursor: '', pagesLoaded: 1, @@ -81,12 +83,16 @@ export default { this.entries.submodules = []; this.entries.blobs = []; this.nextPageCursor = ''; + resetRequestedCommits(); this.fetchFiles(); }, }, mounted() { // We need to wait for `ref` and `projectPath` to be set - this.$nextTick(() => this.fetchFiles()); + this.$nextTick(() => { + resetRequestedCommits(); + this.fetchFiles(); + }); }, methods: { fetchFiles() { @@ -152,6 +158,18 @@ export default { .concat(data.trees.pageInfo, data.submodules.pageInfo, data.blobs.pageInfo) .find(({ hasNextPage }) => hasNextPage); }, + loadCommitData({ rowNumber = 0, hasCommit } = {}) { + if (!this.glFeatures.lazyLoadCommits || hasCommit || isRequested(rowNumber)) { + return; + } + + loadCommits(this.projectPath, this.path, this.ref, rowNumber) + .then(this.setCommitData) + .catch(() => {}); + }, + setCommitData(data) { + this.commits = this.commits.concat(data); + }, handleShowMore() { this.clickedShowMore = true; this.pagesLoaded += 1; @@ -169,7 +187,9 @@ export default { :is-loading="isLoadingFiles" :loading-path="loadingPath" :has-more="hasShowMore" + :commits="commits" @showMore="handleShowMore" + @row-appear="loadCommitData" /> <file-preview v-if="readme" :blob="readme" /> </div> diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue index df5a5ea6163..0199b893453 100644 --- a/app/assets/javascripts/repository/components/upload_blob_modal.vue +++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue @@ -220,6 +220,7 @@ export default { class="gl-h-200! gl-mb-4" single-file-selection :valid-file-mimetypes="$options.validFileMimetypes" + :is-file-valid="() => true" @change="setFile" > <div |