diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-14 15:09:51 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-14 15:09:51 +0300 |
commit | 9223573b85bcfdd21953f52e0d2c5cb587e366a1 (patch) | |
tree | 7dfd09536b948d560fc442014a95a221327b6567 /app | |
parent | 1fc72cb8765dab466da8555b70eb744a53a74a80 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
28 files changed, 613 insertions, 534 deletions
diff --git a/app/assets/javascripts/boards/components/board_card_move_to_position.vue b/app/assets/javascripts/boards/components/board_card_move_to_position.vue index f58f7838576..19eddbfdd68 100644 --- a/app/assets/javascripts/boards/components/board_card_move_to_position.vue +++ b/app/assets/javascripts/boards/components/board_card_move_to_position.vue @@ -14,6 +14,7 @@ export default { GlDisclosureDropdown, }, mixins: [Tracking.mixin()], + inject: ['isApolloBoard'], props: { item: { type: Object, @@ -83,16 +84,20 @@ export default { }); }, moveToPosition({ positionInList }) { - this.moveItem({ - itemId: this.item.id, - itemIid: this.item.iid, - itemPath: this.item.referencePath, - fromListId: this.list.id, - toListId: this.list.id, - positionInList, - atIndex: this.index, - allItemsLoadedInList: !this.listHasNextPage, - }); + if (this.isApolloBoard) { + this.$emit('moveToPosition', positionInList); + } else { + this.moveItem({ + itemId: this.item.id, + itemIid: this.item.iid, + itemPath: this.item.referencePath, + fromListId: this.list.id, + toListId: this.list.id, + positionInList, + atIndex: this.index, + allItemsLoadedInList: !this.listHasNextPage, + }); + } }, selectMoveAction({ text }) { if (text === BOARD_CARD_MOVE_TO_POSITIONS_START_OPTION) { diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 218711346b0..af309ba9912 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -491,6 +491,53 @@ export default { }); } }, + moveToPosition(positionInList, oldIndex, item) { + this.$apollo.mutate({ + mutation: listIssuablesQueries[this.issuableType].moveMutation, + variables: { + ...moveItemVariables({ + iid: item.iid, + epicId: item.id, + fromListId: this.currentList.id, + toListId: this.currentList.id, + isIssue: !this.isEpicBoard, + boardId: this.boardId, + itemToMove: item, + }), + positionInList, + withColor: this.isEpicBoard && this.glFeatures.epicColorHighlight, + }, + optimisticResponse: { + issuableMoveList: { + issuable: item, + errors: [], + }, + }, + update: (cache, { data: { issuableMoveList } }) => { + const { issuable } = issuableMoveList; + removeItemFromList({ + query: listIssuablesQueries[this.issuableType].query, + variables: { ...this.listQueryVariables, id: this.currentList.id }, + boardType: this.boardType, + id: issuable.id, + issuableType: this.issuableType, + cache, + }); + if (positionInList === 0 || this.listItemsCount <= this.boardListItems.length) { + const newIndex = positionInList === 0 ? 0 : this.boardListItems.length - 1; + addItemToList({ + query: listIssuablesQueries[this.issuableType].query, + variables: { ...this.listQueryVariables, id: this.currentList.id }, + issuable, + newIndex, + boardType: this.boardType, + issuableType: this.issuableType, + cache, + }); + } + }, + }); + }, }, }; </script> @@ -545,6 +592,7 @@ export default { :index="index" :list="list" :list-items-length="boardListItems.length" + @moveToPosition="moveToPosition($event, index, item)" /> <gl-intersection-observer v-if="isObservableItem(index)" diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue index b45e55fcb4b..41514d2d2f1 100644 --- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue +++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue @@ -125,7 +125,7 @@ export default { return regex.test(this.variable.value); }, canSubmit() { - return this.variableValidationState && this.variable.key !== '' && this.variable.value !== ''; + return this.variableValidationState && this.variable.key !== ''; }, containsVariableReference() { const regex = /\$/; diff --git a/app/assets/javascripts/ci/runner/components/runner_update_form.vue b/app/assets/javascripts/ci/runner/components/runner_update_form.vue index bc044b609a3..6b94e594f1c 100644 --- a/app/assets/javascripts/ci/runner/components/runner_update_form.vue +++ b/app/assets/javascripts/ci/runner/components/runner_update_form.vue @@ -46,6 +46,11 @@ export default { model: null, }; }, + computed: { + runnerType() { + return this.runner?.runnerType; + }, + }, watch: { runner(val) { this.model = runnerToModel(val); @@ -92,7 +97,7 @@ export default { <template> <gl-form @submit.prevent="onSubmit"> <runner-form-fields v-model="model" :loading="loading" /> - <runner-update-cost-factor-fields v-model="model" /> + <runner-update-cost-factor-fields v-model="model" :runner-type="runnerType" /> <div class="gl-mt-6"> <gl-button diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue index 9c1bcf5bf90..8339034fae9 100644 --- a/app/assets/javascripts/design_management/components/list/item.vue +++ b/app/assets/javascripts/design_management/components/list/item.vue @@ -128,10 +128,10 @@ export default { params: { id: filename }, query: $route.query, }" - class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new gl-mb-0" + class="card gl-cursor-pointer text-plain js-design-list-item design-list-item gl-mb-0" > <div - class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative" + class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative gl-rounded-top-base" > <div v-if="icon.name" diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue index bad907049ff..e7308aad785 100644 --- a/app/assets/javascripts/design_management/pages/index.vue +++ b/app/assets/javascripts/design_management/pages/index.vue @@ -506,7 +506,7 @@ export default { > <design-dropzone :enable-drag-behavior="isDraggingDesign" - :class="{ 'design-list-item design-list-item-new': !isDesignListEmpty }" + :class="{ 'design-list-item': !isDesignListEmpty }" :display-as-card="hasDesigns" v-bind="$options.dropzoneProps" data-qa-selector="design_dropzone_content" diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 1ad8091448c..68536d36ac0 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -488,9 +488,8 @@ export const getDiffMode = (diffFile) => { const diffModeKey = Object.keys(diffModes).find((key) => diffFile[`${key}_file`]); return ( diffModes[diffModeKey] || - (diffFile.viewer && - diffFile.viewer.name === diffViewerModes.mode_changed && - diffViewerModes.mode_changed) || + (diffFile.viewer?.name === diffViewerModes.mode_changed && diffViewerModes.mode_changed) || + (diffFile.viewer?.name === diffViewerModes.no_preview && diffViewerModes.no_preview) || diffModes.replaced ); }; diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 3c12ca70c2a..db32079e6b9 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -43,6 +43,15 @@ export default { diffViewerMode() { return this.discussion.diff_file.viewer.name; }, + fileDiffRefs() { + return this.discussion.diff_file.diff_refs; + }, + headSha() { + return (this.fileDiffRefs ? this.fileDiffRefs.head_sha : this.discussion.commit_id) || ''; + }, + baseSha() { + return (this.fileDiffRefs ? this.fileDiffRefs.base_sha : this.discussion.commit_id) || ''; + }, isTextFile() { return this.diffViewerMode === diffViewerModes.text; }, @@ -144,9 +153,9 @@ export default { :diff-mode="diffMode" :diff-viewer-mode="diffViewerMode" :new-path="discussion.diff_file.new_path" - :new-sha="discussion.diff_file.diff_refs.head_sha" + :new-sha="headSha" :old-path="discussion.diff_file.old_path" - :old-sha="discussion.diff_file.diff_refs.base_sha" + :old-sha="baseSha" :file-hash="discussion.diff_file.file_hash" :project-path="projectPath" > diff --git a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue index c00e75db722..4c0b5d0b1f6 100644 --- a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue +++ b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue @@ -1,11 +1,9 @@ <script> -import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; +import { GlCollapsibleListbox } from '@gitlab/ui'; export default { components: { - GlDropdown, - GlDropdownItem, - GlSearchBoxByType, + GlCollapsibleListbox, }, props: { paramsName: { @@ -25,6 +23,7 @@ export default { data() { return { searchTerm: '', + selectedProjectId: this.selectedProject.id, }; }, computed: { @@ -32,49 +31,45 @@ export default { return this.projects === null; }, filteredRepos() { - const lowerCaseSearchTerm = this.searchTerm.toLowerCase(); + if (this.disableRepoDropdown) return []; - return this?.projects.filter(({ name }) => name.toLowerCase().includes(lowerCaseSearchTerm)); + const lowerCaseSearchTerm = this.searchTerm.toLowerCase(); + return this.projects + .filter(({ name }) => name.toLowerCase().includes(lowerCaseSearchTerm)) + .map((project) => ({ text: project.name, value: project.id })); }, inputName() { return `${this.paramsName}_project_id`; }, }, methods: { - onClick(project) { - this.emitTargetProject(project); - }, - emitTargetProject(project) { + emitTargetProject(projectId) { + if (this.disableRepoDropdown) return; + const project = this.projects.find(({ id }) => id === projectId); this.$emit('selectProject', { direction: this.paramsName, project }); }, + onSearch(searchTerm) { + this.searchTerm = searchTerm; + }, }, }; </script> <template> <div> - <input type="hidden" :name="inputName" :value="selectedProject.id" /> - <gl-dropdown - :text="selectedProject.name" + <input type="hidden" :name="inputName" :value="selectedProjectId" /> + <gl-collapsible-listbox + v-model="selectedProjectId" + :toggle-text="selectedProject.name" :header-text="s__(`CompareRevisions|Select target project`)" - class="gl-w-full gl-font-monospace" + class="gl-font-monospace" toggle-class="gl-min-w-0" :disabled="disableRepoDropdown" - > - <template #header> - <gl-search-box-by-type v-if="!disableRepoDropdown" v-model.trim="searchTerm" /> - </template> - <template v-if="!disableRepoDropdown"> - <gl-dropdown-item - v-for="repo in filteredRepos" - :key="repo.id" - is-check-item - :is-checked="selectedProject.id === repo.id" - @click="onClick(repo)" - > - {{ repo.name }} - </gl-dropdown-item> - </template> - </gl-dropdown> + :items="filteredRepos" + block + searchable + @select="emitTargetProject" + @search="onSearch" + /> </div> </template> diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 99ea02aaa4f..33320f59b0f 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -295,7 +295,7 @@ const bindEvents = () => { }); $newProjectForm.on('submit', () => { - $projectPath.val($projectPath.val().trim()); + $projectPath.value = $projectPath.value.trim(); }); const updateUrlPathWarningVisibility = async () => { diff --git a/app/assets/javascripts/repository/components/blob_viewers/index.js b/app/assets/javascripts/repository/components/blob_viewers/index.js index 68b2cf6f3da..a480710f8ac 100644 --- a/app/assets/javascripts/repository/components/blob_viewers/index.js +++ b/app/assets/javascripts/repository/components/blob_viewers/index.js @@ -4,7 +4,7 @@ const viewers = { image: () => import('./image_viewer.vue'), video: () => import('./video_viewer.vue'), empty: () => import('./empty_viewer.vue'), - text: () => import('~/vue_shared/components/source_viewer/source_viewer_deprecated.vue'), + text: () => import('~/vue_shared/components/source_viewer/source_viewer.vue'), pdf: () => import('./pdf_viewer.vue'), lfs: () => import('./lfs_viewer.vue'), audio: () => import('./audio_viewer.vue'), diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue index 0ec4c759acc..287e4f57d01 100644 --- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue @@ -143,7 +143,7 @@ export default { <template> <nav :aria-label="$options.i18n.mainNavigation" class="gl-p-2 gl-relative"> - <ul v-if="hasStaticItems" class="gl-p-0 gl-m-0"> + <ul v-if="hasStaticItems" class="gl-p-0 gl-m-0" data-testid="static-items-section"> <nav-item v-for="item in staticItems" :key="item.id" :item="item" is-static /> </ul> <pinned-section @@ -159,7 +159,7 @@ export default { class="gl-my-2 gl-mx-4" data-testid="main-menu-separator" /> - <ul class="gl-p-0 gl-list-style-none"> + <ul class="gl-p-0 gl-list-style-none" data-testid="non-static-items-section"> <template v-for="item in nonStaticItems"> <menu-section v-if="isSection(item)" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_preparing.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_preparing.vue new file mode 100644 index 00000000000..1dc4270f054 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_preparing.vue @@ -0,0 +1,23 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; + +import { MR_WIDGET_PREPARING_ASYNCHRONOUSLY } from '../../i18n'; + +export default { + name: 'MRWidgetPreparing', + i18n: { + preparing: MR_WIDGET_PREPARING_ASYNCHRONOUSLY, + }, + components: { + GlLoadingIcon, + }, +}; +</script> +<template> + <div class="gl-w-full gl-display-flex gl-p-4"> + <gl-loading-icon size="md" class="gl-pr-4" inline /> + <div class="gl-display-flex gl-align-items-center"> + {{ $options.i18n.preparing }} + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js index 18503720814..db237bc7439 100644 --- a/app/assets/javascripts/vue_merge_request_widget/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/constants.js @@ -182,6 +182,7 @@ export const INVALID_RULES_DOCS_PATH = helpPagePath( ); export const DETAILED_MERGE_STATUS = { + PREPARING: 'PREPARING', MERGEABLE: 'MERGEABLE', CHECKING: 'CHECKING', NOT_OPEN: 'NOT_OPEN', diff --git a/app/assets/javascripts/vue_merge_request_widget/i18n.js b/app/assets/javascripts/vue_merge_request_widget/i18n.js index 5ca56074031..1b5929e31be 100644 --- a/app/assets/javascripts/vue_merge_request_widget/i18n.js +++ b/app/assets/javascripts/vue_merge_request_widget/i18n.js @@ -1,5 +1,9 @@ import { __, s__ } from '~/locale'; +export const MR_WIDGET_PREPARING_ASYNCHRONOUSLY = s__( + 'mrWidget|Your merge request is almost ready!', +); + export const MR_WIDGET_MISSING_BRANCH_WHICH = s__( 'mrWidget|The %{type} branch %{codeStart}%{name}%{codeEnd} does not exist.', ); diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 6e0ee1cb912..e9efc8e75be 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -26,6 +26,7 @@ import ArchivedState from './components/states/mr_widget_archived.vue'; import MrWidgetAutoMergeEnabled from './components/states/mr_widget_auto_merge_enabled.vue'; import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue'; import CheckingState from './components/states/mr_widget_checking.vue'; +import PreparingState from './components/states/mr_widget_preparing.vue'; import ClosedState from './components/states/mr_widget_closed.vue'; import ConflictsState from './components/states/mr_widget_conflicts.vue'; import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue'; @@ -88,6 +89,7 @@ export default { MrWidgetReadyToMerge, ShaMismatch, MrWidgetChecking: CheckingState, + MrWidgetPreparing: PreparingState, MrWidgetUnresolvedDiscussions: UnresolvedDiscussionsState, MrWidgetPipelineBlocked: PipelineBlockedState, MrWidgetPipelineFailed: PipelineFailedState, @@ -199,7 +201,7 @@ export default { ); }, shouldRenderApprovals() { - return this.mr.state !== 'nothingToMerge'; + return !['preparing', 'nothingToMerge'].includes(this.mr.state); }, componentName() { return stateToComponentMap[this.machineState] || classState[this.mr.state]; diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql index a6b35f20776..4366c01e0a2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql @@ -3,6 +3,7 @@ subscription getStateSubscription($issuableId: IssuableID!) { ... on MergeRequest { id detailedMergeStatus + commitCount } } } diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js index cead42b12ae..f90056a8e1a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js @@ -2,7 +2,9 @@ import { DETAILED_MERGE_STATUS } from '../constants'; import { stateKey } from './state_maps'; export default function deviseState() { - if (!this.commitsCount) { + if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.PREPARING) { + return stateKey.preparing; + } else if (!this.commitsCount) { return stateKey.nothingToMerge; } else if (this.projectArchived) { return stateKey.archived; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 19ce523c482..9ddf8241020 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -212,6 +212,7 @@ export default class MergeRequestStore { setGraphqlSubscriptionData(data) { this.detailedMergeStatus = data.detailedMergeStatus; + this.commitsCount = data.commitCount; this.setState(); } diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js index 9dfeaee905c..04468855942 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js @@ -10,6 +10,7 @@ export const stateToComponentMap = { notAllowedToMerge: 'mr-widget-not-allowed', archived: 'mr-widget-archived', checking: 'mr-widget-checking', + preparing: 'mr-widget-preparing', unresolvedDiscussions: 'mr-widget-unresolved-discussions', pipelineBlocked: 'mr-widget-pipeline-blocked', pipelineFailed: 'mr-widget-pipeline-failed', @@ -38,6 +39,7 @@ export const stateKey = { archived: 'archived', missingBranch: 'missingBranch', nothingToMerge: 'nothingToMerge', + preparing: 'preparing', checking: 'checking', conflicts: 'conflicts', draft: 'draft', diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue index d77061d4b31..28a16cd846a 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue @@ -1,53 +1,64 @@ <script> import { GlIntersectionObserver } from '@gitlab/ui'; -import SafeHtml from '~/vue_shared/directives/safe_html'; -import { getPageParamValue, getPageSearchString } from '~/blob/utils'; +import LineHighlighter from '~/blob/line_highlighter'; +import ChunkLine from './chunk_line.vue'; /* * We only highlight the chunk that is currently visible to the user. * By making use of the Intersection Observer API we can determine when a chunk becomes visible and highlight it accordingly. * - * Content that is not visible to the user (i.e. not highlighted) does not need to look nice, - * so by rendering raw (non-highlighted) text, the browser spends less resources on painting - * content that is not immediately relevant. - * Why use plaintext as opposed to hiding content entirely? - * If content is hidden entirely, native find text (⌘ + F) won't work. + * Content that is not visible to the user (i.e. not highlighted) do not need to look nice, + * so by making text transparent and rendering raw (non-highlighted) text, + * the browser spends less resources on painting content that is not immediately relevant. + * + * Why use transparent text as opposed to hiding content entirely? + * 1. If content is hidden entirely, native find text (⌘ + F) won't work. + * 2. When URL contains line numbers, the browser needs to be able to jump to the correct line. */ export default { components: { + ChunkLine, GlIntersectionObserver, }, - directives: { - SafeHtml, - }, props: { - isHighlighted: { + isFirstChunk: { type: Boolean, - required: true, + required: false, + default: false, }, chunkIndex: { type: Number, required: false, default: 0, }, - rawContent: { - type: String, + isHighlighted: { + type: Boolean, required: true, }, - highlightedContent: { + content: { type: String, required: true, }, + startingFrom: { + type: Number, + required: false, + default: 0, + }, totalLines: { type: Number, required: false, default: 0, }, - startingFrom: { + totalChunks: { type: Number, required: false, default: 0, }, + language: { + type: String, + required: false, + default: null, + }, blamePath: { type: String, required: true, @@ -55,36 +66,37 @@ export default { }, data() { return { - hasAppeared: false, isLoading: true, }; }, computed: { - shouldHighlight() { - return Boolean(this.highlightedContent) && (this.hasAppeared || this.isHighlighted); - }, lines() { return this.content.split('\n'); }, - pageSearchString() { - const page = getPageParamValue(this.number); - return getPageSearchString(this.blamePath, page); - }, }, + created() { - if (this.chunkIndex === 0) { - // Display first chunk ASAP in order to improve perceived performance + if (this.isFirstChunk) { this.isLoading = false; return; } - window.requestIdleCallback(() => { + window.requestIdleCallback(async () => { this.isLoading = false; + const { hash } = this.$route; + if (hash && this.totalChunks > 0 && this.totalChunks === this.chunkIndex + 1) { + // when the last chunk is loaded scroll to the hash + await this.$nextTick(); + const lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' }); + lineHighlighter.highlightHash(hash); + } }); }, methods: { handleChunkAppear() { - this.hasAppeared = true; + if (!this.isHighlighted) { + this.$emit('appear', this.chunkIndex); + } }, calculateLineNumber(index) { return this.startingFrom + index + 1; @@ -94,36 +106,28 @@ export default { </script> <template> <gl-intersection-observer @appear="handleChunkAppear"> - <div class="gl-display-flex"> - <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column"> - <div + <div v-if="isHighlighted"> + <chunk-line + v-for="(line, index) in lines" + :key="index" + :number="calculateLineNumber(index)" + :content="line" + :language="language" + :blame-path="blamePath" + /> + </div> + <div v-else-if="!isLoading" class="gl-display-flex gl-text-transparent"> + <div class="gl-display-flex gl-flex-direction-column content-visibility-auto"> + <span v-for="(n, index) in totalLines" + v-once + :id="`L${calculateLineNumber(index)}`" :key="index" - data-testid="line-numbers" - class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers" - > - <a - class="gl-user-select-none gl-shadow-none! file-line-blame" - :href="`${blamePath}${pageSearchString}#L${calculateLineNumber(index)}`" - ></a> - <a - :id="`L${calculateLineNumber(index)}`" - class="gl-user-select-none gl-shadow-none! file-line-num" - :href="`#L${calculateLineNumber(index)}`" - :data-line-number="calculateLineNumber(index)" - > - {{ calculateLineNumber(index) }} - </a> - </div> + data-testid="line-number" + v-text="calculateLineNumber(index)" + ></span> </div> - - <div v-else-if="!isLoading" class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent"> - <!-- Placeholder for line numbers while content is not highlighted --> - </div> - - <pre - class="gl-m-0 gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-0" - ><code v-if="shouldHighlight" v-once v-safe-html="highlightedContent" data-testid="content"></code><code v-else-if="!isLoading" v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre> + <div v-once class="gl-white-space-pre-wrap!" data-testid="content">{{ content }}</div> </div> </gl-intersection-observer> </template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_deprecated.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_deprecated.vue deleted file mode 100644 index 28a16cd846a..00000000000 --- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_deprecated.vue +++ /dev/null @@ -1,133 +0,0 @@ -<script> -import { GlIntersectionObserver } from '@gitlab/ui'; -import LineHighlighter from '~/blob/line_highlighter'; -import ChunkLine from './chunk_line.vue'; - -/* - * We only highlight the chunk that is currently visible to the user. - * By making use of the Intersection Observer API we can determine when a chunk becomes visible and highlight it accordingly. - * - * Content that is not visible to the user (i.e. not highlighted) do not need to look nice, - * so by making text transparent and rendering raw (non-highlighted) text, - * the browser spends less resources on painting content that is not immediately relevant. - * - * Why use transparent text as opposed to hiding content entirely? - * 1. If content is hidden entirely, native find text (⌘ + F) won't work. - * 2. When URL contains line numbers, the browser needs to be able to jump to the correct line. - */ -export default { - components: { - ChunkLine, - GlIntersectionObserver, - }, - props: { - isFirstChunk: { - type: Boolean, - required: false, - default: false, - }, - chunkIndex: { - type: Number, - required: false, - default: 0, - }, - isHighlighted: { - type: Boolean, - required: true, - }, - content: { - type: String, - required: true, - }, - startingFrom: { - type: Number, - required: false, - default: 0, - }, - totalLines: { - type: Number, - required: false, - default: 0, - }, - totalChunks: { - type: Number, - required: false, - default: 0, - }, - language: { - type: String, - required: false, - default: null, - }, - blamePath: { - type: String, - required: true, - }, - }, - data() { - return { - isLoading: true, - }; - }, - computed: { - lines() { - return this.content.split('\n'); - }, - }, - - created() { - if (this.isFirstChunk) { - this.isLoading = false; - return; - } - - window.requestIdleCallback(async () => { - this.isLoading = false; - const { hash } = this.$route; - if (hash && this.totalChunks > 0 && this.totalChunks === this.chunkIndex + 1) { - // when the last chunk is loaded scroll to the hash - await this.$nextTick(); - const lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' }); - lineHighlighter.highlightHash(hash); - } - }); - }, - methods: { - handleChunkAppear() { - if (!this.isHighlighted) { - this.$emit('appear', this.chunkIndex); - } - }, - calculateLineNumber(index) { - return this.startingFrom + index + 1; - }, - }, -}; -</script> -<template> - <gl-intersection-observer @appear="handleChunkAppear"> - <div v-if="isHighlighted"> - <chunk-line - v-for="(line, index) in lines" - :key="index" - :number="calculateLineNumber(index)" - :content="line" - :language="language" - :blame-path="blamePath" - /> - </div> - <div v-else-if="!isLoading" class="gl-display-flex gl-text-transparent"> - <div class="gl-display-flex gl-flex-direction-column content-visibility-auto"> - <span - v-for="(n, index) in totalLines" - v-once - :id="`L${calculateLineNumber(index)}`" - :key="index" - data-testid="line-number" - v-text="calculateLineNumber(index)" - ></span> - </div> - <div v-once class="gl-white-space-pre-wrap!" data-testid="content">{{ content }}</div> - </div> - </gl-intersection-observer> -</template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue new file mode 100644 index 00000000000..d77061d4b31 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue @@ -0,0 +1,129 @@ +<script> +import { GlIntersectionObserver } from '@gitlab/ui'; +import SafeHtml from '~/vue_shared/directives/safe_html'; +import { getPageParamValue, getPageSearchString } from '~/blob/utils'; + +/* + * We only highlight the chunk that is currently visible to the user. + * By making use of the Intersection Observer API we can determine when a chunk becomes visible and highlight it accordingly. + * + * Content that is not visible to the user (i.e. not highlighted) does not need to look nice, + * so by rendering raw (non-highlighted) text, the browser spends less resources on painting + * content that is not immediately relevant. + * Why use plaintext as opposed to hiding content entirely? + * If content is hidden entirely, native find text (⌘ + F) won't work. + */ +export default { + components: { + GlIntersectionObserver, + }, + directives: { + SafeHtml, + }, + props: { + isHighlighted: { + type: Boolean, + required: true, + }, + chunkIndex: { + type: Number, + required: false, + default: 0, + }, + rawContent: { + type: String, + required: true, + }, + highlightedContent: { + type: String, + required: true, + }, + totalLines: { + type: Number, + required: false, + default: 0, + }, + startingFrom: { + type: Number, + required: false, + default: 0, + }, + blamePath: { + type: String, + required: true, + }, + }, + data() { + return { + hasAppeared: false, + isLoading: true, + }; + }, + computed: { + shouldHighlight() { + return Boolean(this.highlightedContent) && (this.hasAppeared || this.isHighlighted); + }, + lines() { + return this.content.split('\n'); + }, + pageSearchString() { + const page = getPageParamValue(this.number); + return getPageSearchString(this.blamePath, page); + }, + }, + created() { + if (this.chunkIndex === 0) { + // Display first chunk ASAP in order to improve perceived performance + this.isLoading = false; + return; + } + + window.requestIdleCallback(() => { + this.isLoading = false; + }); + }, + methods: { + handleChunkAppear() { + this.hasAppeared = true; + }, + calculateLineNumber(index) { + return this.startingFrom + index + 1; + }, + }, +}; +</script> +<template> + <gl-intersection-observer @appear="handleChunkAppear"> + <div class="gl-display-flex"> + <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column"> + <div + v-for="(n, index) in totalLines" + :key="index" + data-testid="line-numbers" + class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers" + > + <a + class="gl-user-select-none gl-shadow-none! file-line-blame" + :href="`${blamePath}${pageSearchString}#L${calculateLineNumber(index)}`" + ></a> + <a + :id="`L${calculateLineNumber(index)}`" + class="gl-user-select-none gl-shadow-none! file-line-num" + :href="`#L${calculateLineNumber(index)}`" + :data-line-number="calculateLineNumber(index)" + > + {{ calculateLineNumber(index) }} + </a> + </div> + </div> + + <div v-else-if="!isLoading" class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent"> + <!-- Placeholder for line numbers while content is not highlighted --> + </div> + + <pre + class="gl-m-0 gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-0" + ><code v-if="shouldHighlight" v-once v-safe-html="highlightedContent" data-testid="content"></code><code v-else-if="!isLoading" v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre> + </div> + </gl-intersection-observer> +</template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue index 34ef85978b3..9dc6dc1b93a 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -1,41 +1,201 @@ <script> -import SafeHtml from '~/vue_shared/directives/safe_html'; -import Tracking from '~/tracking'; +import { GlLoadingIcon } from '@gitlab/ui'; +import LineHighlighter from '~/blob/line_highlighter'; +import eventHub from '~/notes/event_hub'; +import languageLoader from '~/content_editor/services/highlight_js_language_loader'; import addBlobLinksTracking from '~/blob/blob_links_tracking'; -import { EVENT_ACTION, EVENT_LABEL_VIEWER } from './constants'; +import Tracking from '~/tracking'; +import { + EVENT_ACTION, + EVENT_LABEL_VIEWER, + EVENT_LABEL_FALLBACK, + ROUGE_TO_HLJS_LANGUAGE_MAP, + LINES_PER_CHUNK, + LEGACY_FALLBACKS, + CODEOWNERS_FILE_NAME, + CODEOWNERS_LANGUAGE, +} from './constants'; import Chunk from './components/chunk.vue'; +import { registerPlugins } from './plugins/index'; +/* + * This component is optimized to handle source code with many lines of code by splitting source code into chunks of 70 lines of code, + * we highlight and display the 1st chunk (L1-70) to the user as quickly as possible. + * + * The rest of the lines (L71+) is rendered once the browser goes into an idle state (requestIdleCallback). + * Each chunk is self-contained, this ensures when for example the width of a container on line 1000 changes, + * it does not trigger a repaint on a parent element that wraps all 1000 lines. + */ export default { name: 'SourceViewer', components: { + GlLoadingIcon, Chunk, }, - directives: { - SafeHtml, - }, mixins: [Tracking.mixin()], - inject: { - highlightWorker: { default: null }, - }, props: { blob: { type: Object, required: true, }, - chunks: { - type: Array, - required: false, - default: () => [], + }, + data() { + return { + languageDefinition: null, + content: this.blob.rawTextBlob, + hljs: null, + firstChunk: null, + chunks: {}, + isLoading: true, + isLineSelected: false, + lineHighlighter: null, + }; + }, + computed: { + splitContent() { + return this.content.split(/\r?\n/); + }, + language() { + return this.blob.name === this.$options.codeownersFileName + ? this.$options.codeownersLanguage + : ROUGE_TO_HLJS_LANGUAGE_MAP[this.blob.language?.toLowerCase()]; + }, + lineNumbers() { + return this.splitContent.length; + }, + unsupportedLanguage() { + const supportedLanguages = Object.keys(languageLoader); + const unsupportedLanguage = + !supportedLanguages.includes(this.language) && + !supportedLanguages.includes(this.blob.language?.toLowerCase()); + + return LEGACY_FALLBACKS.includes(this.language) || unsupportedLanguage; + }, + totalChunks() { + return Object.keys(this.chunks).length; }, }, - created() { - this.track(EVENT_ACTION, { label: EVENT_LABEL_VIEWER, property: this.blob.language }); + async created() { addBlobLinksTracking(); + this.trackEvent(EVENT_LABEL_VIEWER); + + if (this.unsupportedLanguage) { + this.handleUnsupportedLanguage(); + return; + } + + this.generateFirstChunk(); + this.hljs = await this.loadHighlightJS(); + + if (this.language) { + this.languageDefinition = await this.loadLanguage(); + } + + // Highlight the first chunk as soon as highlight.js is available + this.highlightChunk(null, true); + + window.requestIdleCallback(async () => { + // Generate the remaining chunks once the browser idles to ensure the browser resources are spent on the most important things first + this.generateRemainingChunks(); + this.isLoading = false; + await this.$nextTick(); + this.lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' }); + }); + }, + methods: { + trackEvent(label) { + this.track(EVENT_ACTION, { label, property: this.blob.language }); + }, + handleUnsupportedLanguage() { + this.trackEvent(EVENT_LABEL_FALLBACK); + this.$emit('error'); + }, + generateFirstChunk() { + const lines = this.splitContent.splice(0, LINES_PER_CHUNK); + this.firstChunk = this.createChunk(lines); + }, + generateRemainingChunks() { + const result = {}; + for (let i = 0; i < this.splitContent.length; i += LINES_PER_CHUNK) { + const chunkIndex = Math.floor(i / LINES_PER_CHUNK); + const lines = this.splitContent.slice(i, i + LINES_PER_CHUNK); + result[chunkIndex] = this.createChunk(lines, i + LINES_PER_CHUNK); + } + + this.chunks = result; + }, + createChunk(lines, startingFrom = 0) { + return { + content: lines.join('\n'), + startingFrom, + totalLines: lines.length, + language: this.language, + isHighlighted: false, + }; + }, + highlightChunk(index, isFirstChunk) { + const chunk = isFirstChunk ? this.firstChunk : this.chunks[index]; + + if (chunk.isHighlighted) { + return; + } + + const { highlightedContent, language } = this.highlight(chunk.content, this.language); + + Object.assign(chunk, { language, content: highlightedContent, isHighlighted: true }); + + this.selectLine(); + + this.$nextTick(() => eventHub.$emit('showBlobInteractionZones', this.blob.path)); + }, + highlight(content, language) { + let detectedLanguage = language; + let highlightedContent; + if (this.hljs) { + registerPlugins(this.hljs, this.blob.fileType, this.content); + if (!detectedLanguage) { + const hljsHighlightAuto = this.hljs.highlightAuto(content); + highlightedContent = hljsHighlightAuto.value; + detectedLanguage = hljsHighlightAuto.language; + } else if (this.languageDefinition) { + highlightedContent = this.hljs.highlight(content, { language: this.language }).value; + } + } + + return { highlightedContent, language: detectedLanguage }; + }, + loadHighlightJS() { + // If no language can be mapped to highlight.js we load all common languages else we load only the core (smallest footprint) + return !this.language ? import('highlight.js/lib/common') : import('highlight.js/lib/core'); + }, + async loadLanguage() { + let languageDefinition; + + try { + languageDefinition = await languageLoader[this.language](); + this.hljs.registerLanguage(this.language, languageDefinition.default); + } catch (message) { + this.$emit('error', message); + } + + return languageDefinition; + }, + async selectLine() { + if (this.isLineSelected || !this.lineHighlighter) { + return; + } + + this.isLineSelected = true; + await this.$nextTick(); + this.lineHighlighter.highlightHash(this.$route.hash); + }, }, userColorScheme: window.gon.user_color_scheme, + currentlySelectedLine: null, + codeownersFileName: CODEOWNERS_FILE_NAME, + codeownersLanguage: CODEOWNERS_LANGUAGE, }; </script> - <template> <div class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto" @@ -45,15 +205,32 @@ export default { data-qa-selector="blob_viewer_file_content" > <chunk - v-for="(chunk, _, index) in chunks" - :key="index" - :chunk-index="index" - :is-highlighted="Boolean(chunk.isHighlighted)" - :raw-content="chunk.rawContent" - :highlighted-content="chunk.highlightedContent" + v-if="firstChunk" + :lines="firstChunk.lines" + :total-lines="firstChunk.totalLines" + :content="firstChunk.content" + :starting-from="firstChunk.startingFrom" + :is-highlighted="firstChunk.isHighlighted" + is-first-chunk + :language="firstChunk.language" + :blame-path="blob.blamePath" + /> + + <gl-loading-icon v-if="isLoading" size="sm" class="gl-my-5" /> + <chunk + v-for="(chunk, key, index) in chunks" + v-else + :key="key" + :lines="chunk.lines" + :content="chunk.content" :total-lines="chunk.totalLines" :starting-from="chunk.startingFrom" + :is-highlighted="chunk.isHighlighted" + :chunk-index="index" + :language="chunk.language" :blame-path="blob.blamePath" + :total-chunks="totalChunks" + @appear="highlightChunk" /> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_deprecated.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_deprecated.vue deleted file mode 100644 index 02b1113e3e9..00000000000 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_deprecated.vue +++ /dev/null @@ -1,236 +0,0 @@ -<script> -import { GlLoadingIcon } from '@gitlab/ui'; -import LineHighlighter from '~/blob/line_highlighter'; -import eventHub from '~/notes/event_hub'; -import languageLoader from '~/content_editor/services/highlight_js_language_loader'; -import addBlobLinksTracking from '~/blob/blob_links_tracking'; -import Tracking from '~/tracking'; -import { - EVENT_ACTION, - EVENT_LABEL_VIEWER, - EVENT_LABEL_FALLBACK, - ROUGE_TO_HLJS_LANGUAGE_MAP, - LINES_PER_CHUNK, - LEGACY_FALLBACKS, - CODEOWNERS_FILE_NAME, - CODEOWNERS_LANGUAGE, -} from './constants'; -import Chunk from './components/chunk_deprecated.vue'; -import { registerPlugins } from './plugins/index'; - -/* - * This component is optimized to handle source code with many lines of code by splitting source code into chunks of 70 lines of code, - * we highlight and display the 1st chunk (L1-70) to the user as quickly as possible. - * - * The rest of the lines (L71+) is rendered once the browser goes into an idle state (requestIdleCallback). - * Each chunk is self-contained, this ensures when for example the width of a container on line 1000 changes, - * it does not trigger a repaint on a parent element that wraps all 1000 lines. - */ -export default { - name: 'SourceViewerDeprecated', - components: { - GlLoadingIcon, - Chunk, - }, - mixins: [Tracking.mixin()], - props: { - blob: { - type: Object, - required: true, - }, - }, - data() { - return { - languageDefinition: null, - content: this.blob.rawTextBlob, - hljs: null, - firstChunk: null, - chunks: {}, - isLoading: true, - isLineSelected: false, - lineHighlighter: null, - }; - }, - computed: { - splitContent() { - return this.content.split(/\r?\n/); - }, - language() { - return this.blob.name === this.$options.codeownersFileName - ? this.$options.codeownersLanguage - : ROUGE_TO_HLJS_LANGUAGE_MAP[this.blob.language?.toLowerCase()]; - }, - lineNumbers() { - return this.splitContent.length; - }, - unsupportedLanguage() { - const supportedLanguages = Object.keys(languageLoader); - const unsupportedLanguage = - !supportedLanguages.includes(this.language) && - !supportedLanguages.includes(this.blob.language?.toLowerCase()); - - return LEGACY_FALLBACKS.includes(this.language) || unsupportedLanguage; - }, - totalChunks() { - return Object.keys(this.chunks).length; - }, - }, - async created() { - addBlobLinksTracking(); - this.trackEvent(EVENT_LABEL_VIEWER); - - if (this.unsupportedLanguage) { - this.handleUnsupportedLanguage(); - return; - } - - this.generateFirstChunk(); - this.hljs = await this.loadHighlightJS(); - - if (this.language) { - this.languageDefinition = await this.loadLanguage(); - } - - // Highlight the first chunk as soon as highlight.js is available - this.highlightChunk(null, true); - - window.requestIdleCallback(async () => { - // Generate the remaining chunks once the browser idles to ensure the browser resources are spent on the most important things first - this.generateRemainingChunks(); - this.isLoading = false; - await this.$nextTick(); - this.lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' }); - }); - }, - methods: { - trackEvent(label) { - this.track(EVENT_ACTION, { label, property: this.blob.language }); - }, - handleUnsupportedLanguage() { - this.trackEvent(EVENT_LABEL_FALLBACK); - this.$emit('error'); - }, - generateFirstChunk() { - const lines = this.splitContent.splice(0, LINES_PER_CHUNK); - this.firstChunk = this.createChunk(lines); - }, - generateRemainingChunks() { - const result = {}; - for (let i = 0; i < this.splitContent.length; i += LINES_PER_CHUNK) { - const chunkIndex = Math.floor(i / LINES_PER_CHUNK); - const lines = this.splitContent.slice(i, i + LINES_PER_CHUNK); - result[chunkIndex] = this.createChunk(lines, i + LINES_PER_CHUNK); - } - - this.chunks = result; - }, - createChunk(lines, startingFrom = 0) { - return { - content: lines.join('\n'), - startingFrom, - totalLines: lines.length, - language: this.language, - isHighlighted: false, - }; - }, - highlightChunk(index, isFirstChunk) { - const chunk = isFirstChunk ? this.firstChunk : this.chunks[index]; - - if (chunk.isHighlighted) { - return; - } - - const { highlightedContent, language } = this.highlight(chunk.content, this.language); - - Object.assign(chunk, { language, content: highlightedContent, isHighlighted: true }); - - this.selectLine(); - - this.$nextTick(() => eventHub.$emit('showBlobInteractionZones', this.blob.path)); - }, - highlight(content, language) { - let detectedLanguage = language; - let highlightedContent; - if (this.hljs) { - registerPlugins(this.hljs, this.blob.fileType, this.content); - if (!detectedLanguage) { - const hljsHighlightAuto = this.hljs.highlightAuto(content); - highlightedContent = hljsHighlightAuto.value; - detectedLanguage = hljsHighlightAuto.language; - } else if (this.languageDefinition) { - highlightedContent = this.hljs.highlight(content, { language: this.language }).value; - } - } - - return { highlightedContent, language: detectedLanguage }; - }, - loadHighlightJS() { - // If no language can be mapped to highlight.js we load all common languages else we load only the core (smallest footprint) - return !this.language ? import('highlight.js/lib/common') : import('highlight.js/lib/core'); - }, - async loadLanguage() { - let languageDefinition; - - try { - languageDefinition = await languageLoader[this.language](); - this.hljs.registerLanguage(this.language, languageDefinition.default); - } catch (message) { - this.$emit('error', message); - } - - return languageDefinition; - }, - async selectLine() { - if (this.isLineSelected || !this.lineHighlighter) { - return; - } - - this.isLineSelected = true; - await this.$nextTick(); - this.lineHighlighter.highlightHash(this.$route.hash); - }, - }, - userColorScheme: window.gon.user_color_scheme, - currentlySelectedLine: null, - codeownersFileName: CODEOWNERS_FILE_NAME, - codeownersLanguage: CODEOWNERS_LANGUAGE, -}; -</script> -<template> - <div - class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto" - :class="$options.userColorScheme" - data-type="simple" - :data-path="blob.path" - data-qa-selector="blob_viewer_file_content" - > - <chunk - v-if="firstChunk" - :lines="firstChunk.lines" - :total-lines="firstChunk.totalLines" - :content="firstChunk.content" - :starting-from="firstChunk.startingFrom" - :is-highlighted="firstChunk.isHighlighted" - is-first-chunk - :language="firstChunk.language" - :blame-path="blob.blamePath" - /> - - <gl-loading-icon v-if="isLoading" size="sm" class="gl-my-5" /> - <chunk - v-for="(chunk, key, index) in chunks" - v-else - :key="key" - :lines="chunk.lines" - :content="chunk.content" - :total-lines="chunk.totalLines" - :starting-from="chunk.startingFrom" - :is-highlighted="chunk.isHighlighted" - :chunk-index="index" - :language="chunk.language" - :blame-path="blob.blamePath" - :total-chunks="totalChunks" - @appear="highlightChunk" - /> - </div> -</template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue new file mode 100644 index 00000000000..7e18c8414d5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue @@ -0,0 +1,64 @@ +<script> +import SafeHtml from '~/vue_shared/directives/safe_html'; +import Tracking from '~/tracking'; +import addBlobLinksTracking from '~/blob/blob_links_tracking'; +import { EVENT_ACTION, EVENT_LABEL_VIEWER } from './constants'; +import Chunk from './components/chunk_new.vue'; + +/* + * Note, this is a new experimental version of the SourceViewer, it is not ready for production use. + * See the following issue for more details: https://gitlab.com/gitlab-org/gitlab/-/issues/391586 + */ + +export default { + name: 'SourceViewerNew', + components: { + Chunk, + }, + directives: { + SafeHtml, + }, + mixins: [Tracking.mixin()], + inject: { + highlightWorker: { default: null }, + }, + props: { + blob: { + type: Object, + required: true, + }, + chunks: { + type: Array, + required: false, + default: () => [], + }, + }, + created() { + this.track(EVENT_ACTION, { label: EVENT_LABEL_VIEWER, property: this.blob.language }); + addBlobLinksTracking(); + }, + userColorScheme: window.gon.user_color_scheme, +}; +</script> + +<template> + <div + class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto" + :class="$options.userColorScheme" + data-type="simple" + :data-path="blob.path" + data-qa-selector="blob_viewer_file_content" + > + <chunk + v-for="(chunk, _, index) in chunks" + :key="index" + :chunk-index="index" + :is-highlighted="Boolean(chunk.isHighlighted)" + :raw-content="chunk.rawContent" + :highlighted-content="chunk.highlightedContent" + :total-lines="chunk.totalLines" + :starting-from="chunk.startingFrom" + :blame-path="blob.blamePath" + /> + </div> +</template> diff --git a/app/assets/stylesheets/page_bundles/design_management.scss b/app/assets/stylesheets/page_bundles/design_management.scss index b42e6fd85fa..15363e78eb3 100644 --- a/app/assets/stylesheets/page_bundles/design_management.scss +++ b/app/assets/stylesheets/page_bundles/design_management.scss @@ -29,7 +29,7 @@ $t-gray-a-16-design-pin: rgba($black, 0.16); } .design-list-item { - height: 280px; + height: 160px; text-decoration: none; .icon-version-status { @@ -37,15 +37,6 @@ $t-gray-a-16-design-pin: rgba($black, 0.16); right: 10px; top: 10px; } - - .card-body { - height: 230px; - } -} - -// This is temporary class to be removed after feature flag removal: https://gitlab.com/gitlab-org/gitlab/-/issues/223197 -.design-list-item-new { - height: 210px; } .design-note-pin { diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index 24003111f88..879c94ae4d4 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -72,17 +72,10 @@ class GroupsFinder < UnionFinder # rubocop: disable CodeReuse/ActiveRecord def groups_with_min_access_level - groups = current_user + current_user .groups .where('members.access_level >= ?', params[:min_access_level]) - - if Feature.enabled?(:use_traversal_ids_groups_finder, current_user) - groups.self_and_descendants - else - Gitlab::ObjectHierarchy - .new(groups) - .base_and_descendants - end + .self_and_descendants end # rubocop: enable CodeReuse/ActiveRecord @@ -145,20 +138,13 @@ class GroupsFinder < UnionFinder def get_groups_for_user groups = [] - if Feature.enabled?(:use_traversal_ids_groups_finder, current_user) - groups << if include_ancestors? - current_user.authorized_groups.self_and_ancestors - else - current_user.authorized_groups - end + groups << if include_ancestors? + current_user.authorized_groups.self_and_ancestors + else + current_user.authorized_groups + end - groups << current_user.groups.self_and_descendants - elsif include_ancestors? - groups << Gitlab::ObjectHierarchy.new(groups_for_ancestors, groups_for_descendants).all_objects - else - groups << current_user.authorized_groups - groups << Gitlab::ObjectHierarchy.new(groups_for_descendants).base_and_descendants - end + groups << current_user.groups.self_and_descendants groups end |