diff options
Diffstat (limited to 'app/assets/javascripts/repository/components')
10 files changed, 348 insertions, 95 deletions
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index 101625a4b72..236351005e7 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -4,7 +4,7 @@ import { uniqueId } from 'lodash'; 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 { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; diff --git a/app/assets/javascripts/repository/components/blob_controls.vue b/app/assets/javascripts/repository/components/blob_controls.vue index 29c2c3762fc..d3e306619bf 100644 --- a/app/assets/javascripts/repository/components/blob_controls.vue +++ b/app/assets/javascripts/repository/components/blob_controls.vue @@ -1,7 +1,7 @@ <script> import { GlButton } from '@gitlab/ui'; import { __ } from '~/locale'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import getRefMixin from '~/repository/mixins/get_ref'; import initSourcegraph from '~/sourcegraph'; import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob'; diff --git a/app/assets/javascripts/repository/components/blob_viewers/notebook_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/notebook_viewer.vue index 1114a0942ec..53dad19028d 100644 --- a/app/assets/javascripts/repository/components/blob_viewers/notebook_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_viewers/notebook_viewer.vue @@ -1,11 +1,15 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; -import notebookLoader from '~/blob/notebook'; import { stripPathTail } from '~/lib/utils/url_utility'; +import NotebookViewer from '~/blob/notebook/notebook_viewer.vue'; export default { - components: { - GlLoadingIcon, + components: { NotebookViewer }, + provide() { + // `relativeRawPath` is injected in app/assets/javascripts/notebook/cells/markdown.vue + // It is needed for images in Markdown cells that reference local files to work. + // See the following MR for more context: + // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69075 + return { relativeRawPath: stripPathTail(this.url) }; }, props: { blob: { @@ -18,14 +22,9 @@ export default { url: this.blob.rawPath, }; }, - mounted() { - notebookLoader({ el: this.$refs.viewer, relativeRawPath: stripPathTail(this.url) }); - }, }; </script> <template> - <div ref="viewer" :data-endpoint="url" data-testid="notebook"> - <gl-loading-icon class="gl-my-4" size="lg" /> - </div> + <notebook-viewer :endpoint="url" /> </template> diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue index baf8449b188..cbdf6ef9ccd 100644 --- a/app/assets/javascripts/repository/components/delete_blob_modal.vue +++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue @@ -101,23 +101,19 @@ export default { primaryOptions() { return { text: this.$options.i18n.PRIMARY_OPTIONS_TEXT, - attributes: [ - { - variant: 'danger', - loading: this.loading, - disabled: this.loading || !this.form.state, - }, - ], + attributes: { + variant: 'danger', + loading: this.loading, + disabled: this.loading || !this.form.state, + }, }; }, cancelOptions() { return { text: this.$options.i18n.SECONDARY_OPTIONS_TEXT, - attributes: [ - { - disabled: this.loading, - }, - ], + attributes: { + disabled: this.loading, + }, }; }, showCreateNewMrToggle() { diff --git a/app/assets/javascripts/repository/components/fork_info.vue b/app/assets/javascripts/repository/components/fork_info.vue index 9804837b200..1a834ba1d82 100644 --- a/app/assets/javascripts/repository/components/fork_info.vue +++ b/app/assets/javascripts/repository/components/fork_info.vue @@ -1,8 +1,16 @@ <script> -import { GlIcon, GlLink, GlSkeletonLoader, GlSprintf } from '@gitlab/ui'; +import { GlIcon, GlLink, GlSkeletonLoader, GlLoadingIcon, GlSprintf, GlButton } from '@gitlab/ui'; import { s__, sprintf, n__ } from '~/locale'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; +import syncForkMutation from '~/repository/mutations/sync_fork.mutation.graphql'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { + POLLING_INTERVAL_DEFAULT, + POLLING_INTERVAL_BACKOFF, + FIVE_MINUTES_IN_MS, +} from '../constants'; import forkDetailsQuery from '../queries/fork_details.query.graphql'; +import ConflictsModal from './fork_sync_conflicts_modal.vue'; export const i18n = { forkedFrom: s__('ForkedFromProjectPath|Forked from'), @@ -12,7 +20,9 @@ export const i18n = { behind: s__('ForksDivergence|%{behindLinkStart}%{behind} %{commit_word} behind%{behindLinkEnd}'), ahead: s__('ForksDivergence|%{aheadLinkStart}%{ahead} %{commit_word} ahead%{aheadLinkEnd} of'), behindAhead: s__('ForksDivergence|%{messages} the upstream repository.'), + limitedVisibility: s__('ForksDivergence|Source project has a limited visibility.'), error: s__('ForksDivergence|Failed to fetch fork details. Try again later.'), + sync: s__('ForksDivergence|Update fork'), }; export default { @@ -20,17 +30,19 @@ export default { components: { GlIcon, GlLink, + GlButton, GlSprintf, GlSkeletonLoader, + ConflictsModal, + GlLoadingIcon, }, + mixins: [glFeatureFlagMixin()], apollo: { project: { query: forkDetailsQuery, + notifyOnNetworkStatusChange: true, variables() { - return { - projectPath: this.projectPath, - ref: this.selectedBranch, - }; + return this.forkDetailsQueryVariables; }, skip() { return !this.sourceName; @@ -42,6 +54,12 @@ export default { error, }); }, + result({ loading }) { + this.handlePolingInterval(loading); + }, + pollInterval() { + return this.pollInterval; + }, }, }, props: { @@ -53,6 +71,11 @@ export default { type: String, required: true, }, + sourceDefaultBranch: { + type: String, + required: false, + default: '', + }, sourceName: { type: String, required: false, @@ -76,18 +99,33 @@ export default { }, data() { return { - project: { - forkDetails: { - ahead: null, - behind: null, - }, - }, + project: {}, + currentPollInterval: null, + isSyncTriggered: false, }; }, computed: { + forkDetailsQueryVariables() { + return { + projectPath: this.projectPath, + ref: this.selectedBranch, + }; + }, + pollInterval() { + return this.isSyncing ? this.currentPollInterval : 0; + }, isLoading() { return this.$apollo.queries.project.loading; }, + forkDetails() { + return this.project?.forkDetails; + }, + hasConflicts() { + return this.forkDetails?.hasConflicts; + }, + isSyncing() { + return this.forkDetails?.isSyncing; + }, ahead() { return this.project?.forkDetails?.ahead; }, @@ -107,7 +145,10 @@ export default { }); }, isUnknownDivergence() { - return (!this.ahead && this.ahead !== 0) || (!this.behind && this.behind !== 0); + return this.sourceName && this.ahead === null && this.behind === null; + }, + isUpToDate() { + return this.ahead === 0 && this.behind === 0; }, behindAheadMessage() { const messages = []; @@ -122,7 +163,16 @@ export default { hasBehindAheadMessage() { return this.behindAheadMessage.length > 0; }, + isSyncButtonAvailable() { + return ( + this.glFeatures.synchronizeFork && + ((this.sourceName && this.forkDetails && this.behind) || this.isUnknownDivergence) + ); + }, forkDivergenceMessage() { + if (!this.forkDetails) { + return this.$options.i18n.limitedVisibility; + } if (this.isUnknownDivergence) { return this.$options.i18n.unknown; } @@ -134,6 +184,73 @@ export default { return this.$options.i18n.upToDate; }, }, + watch: { + hasConflicts(newVal) { + if (newVal && this.isSyncTriggered) { + this.showConflictsModal(); + this.isSyncTriggered = false; + } + }, + }, + methods: { + async syncForkWithPolling() { + await this.$apollo.mutate({ + mutation: syncForkMutation, + variables: { + projectPath: this.projectPath, + targetBranch: this.selectedBranch, + }, + error(error) { + createAlert({ + message: error.message, + captureError: true, + error, + }); + }, + update: (store, { data: { projectSyncFork } }) => { + const { details } = projectSyncFork; + + store.writeQuery({ + query: forkDetailsQuery, + variables: this.forkDetailsQueryVariables, + data: { + project: { + id: this.project.id, + forkDetails: details, + }, + }, + }); + }, + }); + }, + showConflictsModal() { + this.$refs.modal.show(); + }, + startSyncing() { + this.isSyncTriggered = true; + this.syncForkWithPolling(); + }, + checkIfSyncIsPossible() { + if (this.hasConflicts) { + this.showConflictsModal(); + } else { + this.startSyncing(); + } + }, + handlePolingInterval(loading) { + if (!loading && this.isSyncing) { + const backoff = POLLING_INTERVAL_BACKOFF; + const interval = this.currentPollInterval; + const newInterval = Math.min(interval * backoff, FIVE_MINUTES_IN_MS); + this.currentPollInterval = this.currentPollInterval + ? newInterval + : POLLING_INTERVAL_DEFAULT; + } + if (this.currentPollInterval === FIVE_MINUTES_IN_MS) { + this.$apollo.queries.forkDetailsQuery.stopPolling(); + } + }, + }, }; </script> @@ -141,23 +258,45 @@ export default { <div class="info-well gl-sm-display-flex gl-flex-direction-column"> <div class="well-segment gl-p-5 gl-w-full gl-display-flex"> <gl-icon name="fork" :size="16" class="gl-display-block gl-m-4 gl-text-center" /> - <div v-if="sourceName"> - {{ $options.i18n.forkedFrom }} - <gl-link data-qa-selector="forked_from_link" :href="sourcePath">{{ sourceName }}</gl-link> - <gl-skeleton-loader v-if="isLoading" :lines="1" /> - <div v-else class="gl-text-secondary" data-testid="divergence-message"> - <gl-sprintf :message="forkDivergenceMessage"> - <template #aheadLink="{ content }"> - <gl-link :href="aheadComparePath">{{ content }}</gl-link> - </template> - <template #behindLink="{ content }"> - <gl-link :href="behindComparePath">{{ content }}</gl-link> - </template> - </gl-sprintf> + <div + class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-grow-1" + > + <div v-if="sourceName"> + {{ $options.i18n.forkedFrom }} + <gl-link data-qa-selector="forked_from_link" :href="sourcePath">{{ sourceName }}</gl-link> + <gl-skeleton-loader v-if="isLoading" :lines="1" /> + <div v-else class="gl-text-secondary" data-testid="divergence-message"> + <gl-sprintf :message="forkDivergenceMessage"> + <template #aheadLink="{ content }"> + <gl-link :href="aheadComparePath">{{ content }}</gl-link> + </template> + <template #behindLink="{ content }"> + <gl-link :href="behindComparePath">{{ content }}</gl-link> + </template> + </gl-sprintf> + </div> </div> - </div> - <div v-else data-testid="inaccessible-project" class="gl-align-items-center gl-display-flex"> - {{ $options.i18n.inaccessibleProject }} + <div + v-else + data-testid="inaccessible-project" + class="gl-align-items-center gl-display-flex" + > + {{ $options.i18n.inaccessibleProject }} + </div> + <gl-button + v-if="isSyncButtonAvailable" + :disabled="forkDetails.isSyncing" + @click="checkIfSyncIsPossible" + > + <gl-loading-icon v-if="forkDetails.isSyncing" class="gl-display-inline" size="sm" /> + <span>{{ $options.i18n.sync }}</span> + </gl-button> + <conflicts-modal + ref="modal" + :source-name="sourceName" + :source-path="sourcePath" + :source-default-branch="sourceDefaultBranch" + /> </div> </div> </div> diff --git a/app/assets/javascripts/repository/components/fork_sync_conflicts_modal.vue b/app/assets/javascripts/repository/components/fork_sync_conflicts_modal.vue new file mode 100644 index 00000000000..0bfb90bb3ec --- /dev/null +++ b/app/assets/javascripts/repository/components/fork_sync_conflicts_modal.vue @@ -0,0 +1,137 @@ +<script> +/* eslint-disable @gitlab/require-i18n-strings */ +import { GlModal, GlButton } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; +import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; +import SafeHtml from '~/vue_shared/directives/safe_html'; +import { getBaseURL } from '~/lib/utils/url_utility'; + +export const i18n = { + modalTitle: s__('ForksDivergence|Resolve merge conflicts manually'), + modalMessage: s__( + 'ForksDivergence|The upstream changes could not be synchronized to this project due to file conflicts in the default branch. You must resolve the conflicts manually:', + ), + step1: __('Step 1.'), + step2: __('Step 2.'), + step3: __('Step 3.'), + step4: __('Step 4.'), + step1Text: s__( + "ForksDivergence|Fetch the latest changes from the upstream repository's default branch:", + ), + step2Text: s__( + "ForksDivergence|Check out to a new branch, and merge the changes from the upstream project's default branch. You likely need to resolve conflicts during this step.", + ), + step3Text: s__('ForksDivergence|Push the updates to remote:'), + step4Text: s__("ForksDivergence|Create a merge request to your project's default branch."), + copyToClipboard: __('Copy to clipboard'), + close: __('Close'), +}; + +export default { + name: 'ForkSyncConflictsModal', + components: { + GlModal, + GlButton, + ModalCopyButton, + }, + directives: { + SafeHtml, + }, + props: { + sourceDefaultBranch: { + type: String, + required: false, + default: '', + }, + sourceName: { + type: String, + required: false, + default: '', + }, + sourcePath: { + type: String, + required: false, + default: '', + }, + }, + computed: { + instructionsStep1() { + const baseUrl = getBaseURL(); + return `git fetch ${baseUrl}${this.sourcePath} ${this.sourceDefaultBranch}`; + }, + }, + methods: { + show() { + this.$refs.modal.show(); + }, + hide() { + this.$refs.modal.hide(); + }, + }, + i18n, + instructionsStep2: 'git checkout -b <new-branch-name>\ngit merge FETCH_HEAD', + instructionsStep2Clipboard: 'git checkout -b <new-branch-name>\ngit merge FETCH_HEAD', + instructionsStep3: 'git commit\ngit push', +}; +</script> +<template> + <gl-modal + ref="modal" + modal-id="fork-sync-conflicts-modal" + :title="$options.i18n.modalTitle" + size="md" + > + <p>{{ $options.i18n.modalMessage }}</p> + <p> + <b> {{ $options.i18n.step1 }}</b> {{ $options.i18n.modalMessage }} + </p> + <div class="gl-display-flex gl-mb-4"> + <pre class="gl-w-full gl-mb-0 gl-mr-3" data-testid="resolve-conflict-instructions">{{ + instructionsStep1 + }}</pre> + <modal-copy-button + modal-id="fork-sync-conflicts-modal" + :text="instructionsStep1" + :title="$options.i18n.copyToClipboard" + class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0" + /> + </div> + <p> + <b> {{ $options.i18n.step2 }}</b> {{ $options.i18n.step2Text }} + </p> + <div class="gl-display-flex gl-mb-4"> + <pre + class="gl-w-full gl-mb-0 gl-mr-3" + data-testid="resolve-conflict-instructions" + v-html="$options.instructionsStep2 /* eslint-disable-line vue/no-v-html */" + ></pre> + <modal-copy-button + modal-id="fork-sync-conflicts-modal" + :text="$options.instructionsStep2Clipboard" + :title="$options.i18n.copyToClipboard" + class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0" + /> + </div> + <p> + <b> {{ $options.i18n.step3 }}</b> {{ $options.i18n.step3Text }} + </p> + <div class="gl-display-flex gl-mb-4"> + <pre class="gl-w-full gl-mb-0" data-testid="resolve-conflict-instructions" + >{{ $options.instructionsStep3 }} +</pre + > + <modal-copy-button + modal-id="fork-sync-conflicts-modal" + :text="$options.instructionsStep3" + :title="$options.i18n.copyToClipboard" + class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0 gl-ml-3" + /> + </div> + <p> + <b> {{ $options.i18n.step4 }}</b> {{ $options.i18n.step4Text }} + </p> + <template #modal-footer> + <gl-button @click="hide" @keydown.esc="hide">{{ $options.i18n.close }}</gl-button> + </template> + </gl-modal> +</template> diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue index 4d3c1521559..2d2e21dfd92 100644 --- a/app/assets/javascripts/repository/components/last_commit.vue +++ b/app/assets/javascripts/repository/components/last_commit.vue @@ -9,6 +9,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import SignatureBadge from '~/commit/components/signature_badge.vue'; import getRefMixin from '../mixins/get_ref'; import projectPathQuery from '../queries/project_path.query.graphql'; @@ -23,6 +24,7 @@ export default { GlLink, GlLoadingIcon, UserAvatarImage, + SignatureBadge, }, directives: { GlTooltip: GlTooltipDirective, @@ -170,10 +172,7 @@ export default { <div class="commit-actions gl-display-flex gl-flex-align gl-align-items-center gl-flex-direction-row" > - <div - v-if="commit.signatureHtml" - v-html="commit.signatureHtml /* eslint-disable-line vue/no-v-html */" - ></div> + <signature-badge v-if="commit.signature" :signature="commit.signature" /> <div v-if="commit.pipeline" class="ci-status-link"> <gl-link v-gl-tooltip.left diff --git a/app/assets/javascripts/repository/components/new_directory_modal.vue b/app/assets/javascripts/repository/components/new_directory_modal.vue index b28ebe7bb1e..f36a700c902 100644 --- a/app/assets/javascripts/repository/components/new_directory_modal.vue +++ b/app/assets/javascripts/repository/components/new_directory_modal.vue @@ -8,7 +8,7 @@ import { GlFormTextarea, GlToggle, } from '@gitlab/ui'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { visitUrl } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; @@ -93,23 +93,19 @@ export default { primaryOptions() { return { text: this.primaryBtnText, - attributes: [ - { - variant: 'confirm', - loading: this.loading, - disabled: !this.formCompleted || this.loading, - }, - ], + attributes: { + variant: 'confirm', + loading: this.loading, + disabled: !this.formCompleted || this.loading, + }, }; }, cancelOptions() { return { text: SECONDARY_OPTIONS_TEXT, - attributes: [ - { - disabled: this.loading, - }, - ], + attributes: { + disabled: this.loading, + }, }; }, showCreateNewMrToggle() { diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue index f6d6004ba96..0c9b46344c5 100644 --- a/app/assets/javascripts/repository/components/tree_content.vue +++ b/app/assets/javascripts/repository/components/tree_content.vue @@ -1,10 +1,8 @@ <script> import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql'; -import { createAlert } from '~/flash'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { createAlert } from '~/alert'; import { TREE_PAGE_SIZE, - TREE_INITIAL_FETCH_COUNT, TREE_PAGE_LIMIT, COMMIT_BATCH_SIZE, GITALY_UNAVAILABLE_CODE, @@ -23,7 +21,7 @@ export default { FileTable, FilePreview, }, - mixins: [getRefMixin, glFeatureFlagMixin()], + mixins: [getRefMixin], apollo: { projectPath: { query: projectPathQuery, @@ -59,13 +57,6 @@ export default { }; }, 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; }, @@ -110,7 +101,7 @@ export default { ref: this.ref, path: originalPath, nextPageCursor: this.nextPageCursor, - pageSize: this.pageSize, + pageSize: TREE_PAGE_SIZE, }, }) .then(({ data }) => { diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue index 4603ea2710d..4ca625bc0de 100644 --- a/app/assets/javascripts/repository/components/upload_blob_modal.vue +++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue @@ -9,7 +9,7 @@ import { GlButton, GlAlert, } from '@gitlab/ui'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { ContentTypeMultipartFormData } from '~/lib/utils/headers'; import { numberToHumanSize } from '~/lib/utils/number_utils'; @@ -106,23 +106,19 @@ export default { primaryOptions() { return { text: this.primaryBtnText, - attributes: [ - { - variant: 'confirm', - loading: this.loading, - disabled: !this.formCompleted || this.loading, - }, - ], + attributes: { + variant: 'confirm', + loading: this.loading, + disabled: !this.formCompleted || this.loading, + }, }; }, cancelOptions() { return { text: SECONDARY_OPTIONS_TEXT, - attributes: [ - { - disabled: this.loading, - }, - ], + attributes: { + disabled: this.loading, + }, }; }, formattedFileSize() { |