summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/repository
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/repository')
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue17
-rw-r--r--app/assets/javascripts/repository/components/blob_header_edit.vue28
-rw-r--r--app/assets/javascripts/repository/components/blob_replace.vue75
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue4
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue38
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue17
-rw-r--r--app/assets/javascripts/repository/components/upload_blob_modal.vue82
-rw-r--r--app/assets/javascripts/repository/constants.js4
-rw-r--r--app/assets/javascripts/repository/graphql.js20
-rw-r--r--app/assets/javascripts/repository/index.js3
-rw-r--r--app/assets/javascripts/repository/log_tree.js32
-rw-r--r--app/assets/javascripts/repository/mixins/get_ref.js2
-rw-r--r--app/assets/javascripts/repository/queries/blob_info.query.graphql3
-rw-r--r--app/assets/javascripts/repository/queries/commit.query.graphql4
14 files changed, 279 insertions, 50 deletions
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index a9701c8f8aa..7fbf331d585 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -8,11 +8,13 @@ import createFlash from '~/flash';
import { __ } from '~/locale';
import blobInfoQuery from '../queries/blob_info.query.graphql';
import BlobHeaderEdit from './blob_header_edit.vue';
+import BlobReplace from './blob_replace.vue';
export default {
components: {
BlobHeader,
BlobHeaderEdit,
+ BlobReplace,
BlobContent,
GlLoadingIcon,
},
@@ -87,6 +89,9 @@ export default {
};
},
computed: {
+ isLoggedIn() {
+ return Boolean(gon.current_user_id);
+ },
isLoading() {
return this.$apollo.queries.project.loading;
},
@@ -126,7 +131,17 @@ export default {
@viewer-changed="switchViewer"
>
<template #actions>
- <blob-header-edit :edit-path="blobInfo.editBlobPath" />
+ <blob-header-edit
+ :edit-path="blobInfo.editBlobPath"
+ :web-ide-path="blobInfo.ideEditPath"
+ />
+ <blob-replace
+ v-if="isLoggedIn"
+ :path="path"
+ :name="blobInfo.name"
+ :replace-path="blobInfo.replacePath"
+ :can-push-code="blobInfo.canModifyBlob"
+ />
</template>
</blob-header>
<blob-content
diff --git a/app/assets/javascripts/repository/components/blob_header_edit.vue b/app/assets/javascripts/repository/components/blob_header_edit.vue
index f3649895736..3d97ebe89e4 100644
--- a/app/assets/javascripts/repository/components/blob_header_edit.vue
+++ b/app/assets/javascripts/repository/components/blob_header_edit.vue
@@ -1,25 +1,47 @@
<script>
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
+import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
i18n: {
edit: __('Edit'),
+ webIde: __('Web IDE'),
},
components: {
GlButton,
+ WebIdeLink,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
editPath: {
type: String,
required: true,
},
+ webIdePath: {
+ type: String,
+ required: true,
+ },
},
};
</script>
<template>
- <gl-button category="primary" variant="confirm" class="gl-mr-3" :href="editPath">
- {{ $options.i18n.edit }}
- </gl-button>
+ <web-ide-link
+ v-if="glFeatures.consolidatedEditButton"
+ class="gl-mr-3"
+ :edit-url="editPath"
+ :web-ide-url="webIdePath"
+ :is-blob="true"
+ />
+ <div v-else>
+ <gl-button class="gl-mr-2" category="primary" variant="confirm" :href="editPath">
+ {{ $options.i18n.edit }}
+ </gl-button>
+
+ <gl-button class="gl-mr-3" category="primary" variant="confirm" :href="webIdePath">
+ {{ $options.i18n.webIde }}
+ </gl-button>
+ </div>
</template>
diff --git a/app/assets/javascripts/repository/components/blob_replace.vue b/app/assets/javascripts/repository/components/blob_replace.vue
new file mode 100644
index 00000000000..91d7811eb6d
--- /dev/null
+++ b/app/assets/javascripts/repository/components/blob_replace.vue
@@ -0,0 +1,75 @@
+<script>
+import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import { sprintf, __ } from '~/locale';
+import getRefMixin from '../mixins/get_ref';
+import UploadBlobModal from './upload_blob_modal.vue';
+
+export default {
+ i18n: {
+ replace: __('Replace'),
+ replacePrimaryBtnText: __('Replace file'),
+ },
+ components: {
+ GlButton,
+ UploadBlobModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ mixins: [getRefMixin],
+ inject: {
+ targetBranch: {
+ default: '',
+ },
+ originalBranch: {
+ default: '',
+ },
+ },
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ replacePath: {
+ type: String,
+ required: true,
+ },
+ canPushCode: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ replaceModalId() {
+ return uniqueId('replace-modal');
+ },
+ title() {
+ return sprintf(__('Replace %{name}'), { name: this.name });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-mr-3">
+ <gl-button v-gl-modal="replaceModalId">
+ {{ $options.i18n.replace }}
+ </gl-button>
+ <upload-blob-modal
+ :modal-id="replaceModalId"
+ :modal-title="title"
+ :commit-message="title"
+ :target-branch="targetBranch || ref"
+ :original-branch="originalBranch || ref"
+ :can-push-code="canPushCode"
+ :path="path"
+ :replace-path="replacePath"
+ :primary-btn-text="$options.i18n.replacePrimaryBtnText"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 22dffb7d2db..ca5711de49c 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -51,6 +51,9 @@ export default {
};
},
computed: {
+ totalEntries() {
+ return Object.values(this.entries).flat().length;
+ },
tableCaption() {
if (this.isLoading) {
return sprintf(
@@ -111,6 +114,7 @@ export default {
:submodule-tree-url="entry.treeUrl"
:lfs-oid="entry.lfsOid"
:loading-path="loadingPath"
+ :total-entries="totalEntries"
/>
</template>
<template v-if="isLoading">
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 8ea5fce92fa..62f863db871 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -7,13 +7,17 @@ import {
GlTooltipDirective,
GlLoadingIcon,
GlIcon,
+ GlHoverLoadDirective,
} from '@gitlab/ui';
import { escapeRegExp } from 'lodash';
+import filesQuery from 'shared_queries/repository/files.query.graphql';
import { escapeFileUrl } from '~/lib/utils/url_utility';
+import { TREE_PAGE_SIZE } from '~/repository/constants';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getRefMixin from '../../mixins/get_ref';
+import blobInfoQuery from '../../queries/blob_info.query.graphql';
import commitQuery from '../../queries/commit.query.graphql';
export default {
@@ -28,6 +32,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ GlHoverLoad: GlHoverLoadDirective,
},
apollo: {
commit: {
@@ -38,12 +43,17 @@ export default {
type: this.type,
path: this.currentPath,
projectPath: this.projectPath,
+ maxOffset: this.totalEntries,
};
},
},
},
mixins: [getRefMixin, glFeatureFlagMixin()],
props: {
+ totalEntries: {
+ type: Number,
+ required: true,
+ },
id: {
type: String,
required: true,
@@ -139,6 +149,33 @@ export default {
return this.commit && this.commit.lockLabel;
},
},
+ methods: {
+ handlePreload() {
+ return this.isFolder ? this.loadFolder() : this.loadBlob();
+ },
+ loadFolder() {
+ this.apolloQuery(filesQuery, {
+ projectPath: this.projectPath,
+ ref: this.ref,
+ path: this.path,
+ nextPageCursor: '',
+ pageSize: TREE_PAGE_SIZE,
+ });
+ },
+ loadBlob() {
+ if (!this.refactorBlobViewerEnabled) {
+ return;
+ }
+
+ this.apolloQuery(blobInfoQuery, {
+ projectPath: this.projectPath,
+ filePath: this.path,
+ });
+ },
+ apolloQuery(query, variables) {
+ this.$apollo.query({ query, variables });
+ },
+ },
};
</script>
@@ -148,6 +185,7 @@ export default {
<component
:is="linkComponent"
ref="link"
+ v-gl-hover-load="handlePreload"
: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 336237abd8a..794a8a85cc5 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -1,17 +1,14 @@
<script>
import filesQuery from 'shared_queries/repository/files.query.graphql';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '../../locale';
+import { TREE_PAGE_SIZE, TREE_INITIAL_FETCH_COUNT } from '../constants';
import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
import { readmeFile } from '../utils/readme';
import FilePreview from './preview/index.vue';
import FileTable from './table/index.vue';
-const LIMIT = 1000;
-const PAGE_SIZE = 100;
-export const INITIAL_FETCH_COUNT = LIMIT / PAGE_SIZE;
-
export default {
components: {
FileTable,
@@ -47,7 +44,7 @@ export default {
isLoadingFiles: false,
isOverLimit: false,
clickedShowMore: false,
- pageSize: PAGE_SIZE,
+ pageSize: TREE_PAGE_SIZE,
fetchCounter: 0,
};
},
@@ -56,7 +53,7 @@ export default {
return readmeFile(this.entries.blobs);
},
hasShowMore() {
- return !this.clickedShowMore && this.fetchCounter === INITIAL_FETCH_COUNT;
+ return !this.clickedShowMore && this.fetchCounter === TREE_INITIAL_FETCH_COUNT;
},
},
@@ -107,14 +104,16 @@ export default {
if (pageInfo?.hasNextPage) {
this.nextPageCursor = pageInfo.endCursor;
this.fetchCounter += 1;
- if (this.fetchCounter < INITIAL_FETCH_COUNT || this.clickedShowMore) {
+ if (this.fetchCounter < TREE_INITIAL_FETCH_COUNT || this.clickedShowMore) {
this.fetchFiles();
this.clickedShowMore = false;
}
}
})
.catch((error) => {
- createFlash(__('An error occurred while fetching folder content.'));
+ createFlash({
+ message: __('An error occurred while fetching folder content.'),
+ });
throw error;
});
},
diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue
index aa087d4c631..7f065dbdf6d 100644
--- a/app/assets/javascripts/repository/components/upload_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue
@@ -43,7 +43,6 @@ export default {
GlAlert,
},
i18n: {
- MODAL_TITLE,
COMMIT_LABEL,
TARGET_BRANCH_LABEL,
TOGGLE_CREATE_MR_LABEL,
@@ -51,6 +50,16 @@ export default {
NEW_BRANCH_IN_FORK,
},
props: {
+ modalTitle: {
+ type: String,
+ default: MODAL_TITLE,
+ required: false,
+ },
+ primaryBtnText: {
+ type: String,
+ default: PRIMARY_OPTIONS_TEXT,
+ required: false,
+ },
modalId: {
type: String,
required: true,
@@ -75,6 +84,11 @@ export default {
type: String,
required: true,
},
+ replacePath: {
+ type: String,
+ default: null,
+ required: false,
+ },
},
data() {
return {
@@ -90,7 +104,7 @@ export default {
computed: {
primaryOptions() {
return {
- text: PRIMARY_OPTIONS_TEXT,
+ text: this.primaryBtnText,
attributes: [
{
variant: 'confirm',
@@ -136,6 +150,45 @@ export default {
this.file = null;
this.filePreviewURL = null;
},
+ submitForm() {
+ return this.replacePath ? this.replaceFile() : this.uploadFile();
+ },
+ submitRequest(method, url) {
+ return axios({
+ method,
+ url,
+ data: this.formData(),
+ headers: {
+ ...ContentTypeMultipartFormData,
+ },
+ })
+ .then((response) => {
+ if (!this.replacePath) {
+ trackFileUploadEvent('click_upload_modal_form_submit');
+ }
+ visitUrl(response.data.filePath);
+ })
+ .catch(() => {
+ this.loading = false;
+ createFlash(ERROR_MESSAGE);
+ });
+ },
+ formData() {
+ const formData = new FormData();
+ formData.append('branch_name', this.target);
+ formData.append('create_merge_request', this.createNewMr);
+ formData.append('commit_message', this.commit);
+ formData.append('file', this.file);
+
+ return formData;
+ },
+ replaceFile() {
+ this.loading = true;
+
+ // The PUT path can be geneated from $route (similar to "uploadFile") once router is connected
+ // Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/332736
+ return this.submitRequest('put', this.replacePath);
+ },
uploadFile() {
this.loading = true;
@@ -146,26 +199,7 @@ export default {
} = this;
const uploadPath = joinPaths(this.path, path);
- const formData = new FormData();
- formData.append('branch_name', this.target);
- formData.append('create_merge_request', this.createNewMr);
- formData.append('commit_message', this.commit);
- formData.append('file', this.file);
-
- return axios
- .post(uploadPath, formData, {
- headers: {
- ...ContentTypeMultipartFormData,
- },
- })
- .then((response) => {
- trackFileUploadEvent('click_upload_modal_form_submit');
- visitUrl(response.data.filePath);
- })
- .catch(() => {
- this.loading = false;
- createFlash(ERROR_MESSAGE);
- });
+ return this.submitRequest('post', uploadPath);
},
},
validFileMimetypes: [],
@@ -175,10 +209,10 @@ export default {
<gl-form>
<gl-modal
:modal-id="modalId"
- :title="$options.i18n.MODAL_TITLE"
+ :title="modalTitle"
:action-primary="primaryOptions"
:action-cancel="cancelOptions"
- @primary.prevent="uploadFile"
+ @primary.prevent="submitForm"
>
<upload-dropzone
class="gl-h-200! gl-mb-4"
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
new file mode 100644
index 00000000000..62d5d3db445
--- /dev/null
+++ b/app/assets/javascripts/repository/constants.js
@@ -0,0 +1,4 @@
+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
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index 4a4b9d115b7..4892e54ebef 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -17,15 +17,21 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
const defaultClient = createDefaultClient(
{
Query: {
- commit(_, { path, fileName, type }) {
+ commit(_, { path, fileName, type, maxOffset }) {
return new Promise((resolve) => {
- fetchLogsTree(defaultClient, path, '0', {
- resolve,
- entry: {
- name: fileName,
- type,
+ fetchLogsTree(
+ defaultClient,
+ path,
+ '0',
+ {
+ resolve,
+ entry: {
+ name: fileName,
+ type,
+ },
},
- });
+ maxOffset,
+ );
});
},
readme(_, { url }) {
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 501ae7e9f2f..60a1a0443f7 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -141,6 +141,9 @@ export default function setupVueRepositoryList() {
href: `${historyLink}/${
this.$route.params.path ? escapeFileUrl(this.$route.params.path) : ''
}`,
+ // Ideally passing this class to `props` should work
+ // But it doesn't work here. :(
+ class: 'btn btn-default btn-md gl-button ml-sm-0',
},
},
[__('History')],
diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js
index 9001bcd8fc3..ac02392d60f 100644
--- a/app/assets/javascripts/repository/log_tree.js
+++ b/app/assets/javascripts/repository/log_tree.js
@@ -7,6 +7,13 @@ import refQuery from './queries/ref.query.graphql';
const fetchpromises = {};
const resolvers = {};
+let maxOffset;
+let nextOffset;
+let currentPath;
+
+function setNextOffset(offset) {
+ nextOffset = offset || null;
+}
export function resolveCommit(commits, path, { resolve, entry }) {
const commit = commits.find(
@@ -18,7 +25,25 @@ export function resolveCommit(commits, path, { resolve, entry }) {
}
}
-export function fetchLogsTree(client, path, offset, resolver = null) {
+export function fetchLogsTree(client, path, offset, resolver = null, _maxOffset = null) {
+ if (_maxOffset) {
+ maxOffset = _maxOffset;
+ }
+
+ if (!currentPath || currentPath !== path) {
+ // ensures the nextOffset is reset if the user changed directories
+ setNextOffset(null);
+ }
+
+ currentPath = path;
+
+ const offsetNumber = Number(offset);
+
+ if (!nextOffset && offsetNumber > maxOffset) {
+ setNextOffset(offsetNumber - 25); // ensures commit data is fetched for newly added rows that need data from the previous request (requests are made in batches of 25).
+ return Promise.resolve();
+ }
+
if (resolver) {
if (!resolvers[path]) {
resolvers[path] = [resolver];
@@ -38,7 +63,7 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
path.replace(/^\//, ''),
)}`,
{
- params: { format: 'json', offset },
+ params: { format: 'json', offset: nextOffset || offset },
},
)
.then(({ data: newData, headers }) => {
@@ -57,9 +82,12 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
delete fetchpromises[path];
if (headerLogsOffset) {
+ setNextOffset(null);
fetchLogsTree(client, path, headerLogsOffset);
} else {
delete resolvers[path];
+ maxOffset = null;
+ setNextOffset(null);
}
});
diff --git a/app/assets/javascripts/repository/mixins/get_ref.js b/app/assets/javascripts/repository/mixins/get_ref.js
index 1f1880a48c7..3247938f999 100644
--- a/app/assets/javascripts/repository/mixins/get_ref.js
+++ b/app/assets/javascripts/repository/mixins/get_ref.js
@@ -6,7 +6,7 @@ export default {
query: refQuery,
manual: true,
result({ data, loading }) {
- if (!loading) {
+ if (data && !loading) {
this.ref = data.ref;
this.escapedRef = data.escapedRef;
}
diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql
index 07c076af54b..bfd9447d260 100644
--- a/app/assets/javascripts/repository/queries/blob_info.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql
@@ -1,6 +1,5 @@
query getBlobInfo($projectPath: ID!, $filePath: String!) {
project(fullPath: $projectPath) {
- id
repository {
blobs(paths: [$filePath]) {
nodes {
@@ -12,9 +11,11 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) {
fileType
path
editBlobPath
+ ideEditPath
storedExternally
rawPath
replacePath
+ canModifyBlob
simpleViewer {
fileType
tooLarge
diff --git a/app/assets/javascripts/repository/queries/commit.query.graphql b/app/assets/javascripts/repository/queries/commit.query.graphql
index e4aeaaff8fe..7ae4a3b984a 100644
--- a/app/assets/javascripts/repository/queries/commit.query.graphql
+++ b/app/assets/javascripts/repository/queries/commit.query.graphql
@@ -1,7 +1,7 @@
#import "ee_else_ce/repository/queries/commit.fragment.graphql"
-query getCommit($fileName: String!, $type: String!, $path: String!) {
- commit(path: $path, fileName: $fileName, type: $type) @client {
+query getCommit($fileName: String!, $type: String!, $path: String!, $maxOffset: Number!) {
+ commit(path: $path, fileName: $fileName, type: $type, maxOffset: $maxOffset) @client {
...TreeEntryCommit
}
}