Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-06-14 15:09:51 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-14 15:09:51 +0300
commit9223573b85bcfdd21953f52e0d2c5cb587e366a1 (patch)
tree7dfd09536b948d560fc442014a95a221327b6567 /app
parent1fc72cb8765dab466da8555b70eb744a53a74a80 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/boards/components/board_card_move_to_position.vue25
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue48
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_update_form.vue7
-rw-r--r--app/assets/javascripts/design_management/components/list/item.vue4
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue2
-rw-r--r--app/assets/javascripts/diffs/store/utils.js5
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue13
-rw-r--r--app/assets/javascripts/projects/compare/components/repo_dropdown.vue55
-rw-r--r--app/assets/javascripts/projects/project_new.js2
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/index.js2
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_menu.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_preparing.vue23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/i18n.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue114
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_deprecated.vue133
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue129
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue221
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_deprecated.vue236
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue64
-rw-r--r--app/assets/stylesheets/page_bundles/design_management.scss11
-rw-r--r--app/finders/groups_finder.rb30
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