Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d7a8615c99
commit
0134e6bc27
|
|
@ -70,6 +70,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isSnippet: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { richContentLoaded: false };
|
||||
|
|
@ -123,6 +128,7 @@ export default {
|
|||
:project-path="projectPath"
|
||||
:blob-path="blob.path || ''"
|
||||
:rich-viewer="richViewer"
|
||||
:is-snippet="isSnippet"
|
||||
:is-raw-content="isRawContent"
|
||||
:show-blame="showBlame"
|
||||
:file-name="blob.name"
|
||||
|
|
|
|||
|
|
@ -119,6 +119,15 @@ export default {
|
|||
listTitle() {
|
||||
return this.list?.label?.description || this.list?.assignee?.name || this.list.title || '';
|
||||
},
|
||||
listStatus() {
|
||||
return this.list?.status || {};
|
||||
},
|
||||
listStatusColor() {
|
||||
return this.listStatus?.color;
|
||||
},
|
||||
listStatusIconName() {
|
||||
return this.listStatus?.iconName;
|
||||
},
|
||||
isIterationList() {
|
||||
return this.listType === ListType.iteration;
|
||||
},
|
||||
|
|
@ -377,6 +386,18 @@ export default {
|
|||
class="gl-mr-3"
|
||||
/>
|
||||
</a>
|
||||
<gl-icon
|
||||
v-if="listType === 'status'"
|
||||
data-testid="status-icon"
|
||||
:name="listStatusIconName"
|
||||
:size="12"
|
||||
:class="{
|
||||
'gl-mt-2': list.collapsed,
|
||||
'gl-mx-2': !list.collapsed,
|
||||
'gl-shrink-0': true,
|
||||
}"
|
||||
:style="{ color: listStatusColor }"
|
||||
/>
|
||||
<!-- EE end -->
|
||||
<div
|
||||
class="board-title-text"
|
||||
|
|
@ -387,6 +408,7 @@ export default {
|
|||
}"
|
||||
>
|
||||
<!-- EE start -->
|
||||
|
||||
<span
|
||||
v-if="listType !== 'label'"
|
||||
v-gl-tooltip.hover
|
||||
|
|
|
|||
|
|
@ -242,6 +242,10 @@ export default {
|
|||
this.emitEvents();
|
||||
}
|
||||
},
|
||||
selectAll() {
|
||||
const allInputs = this.searchFilteredInputs.map((input) => input.value);
|
||||
this.selectInputs(allInputs);
|
||||
},
|
||||
deselectAll() {
|
||||
this.inputs = this.inputs.map((input) => ({
|
||||
...input,
|
||||
|
|
@ -277,6 +281,7 @@ export default {
|
|||
:toggle-text="s__('Pipelines|Select inputs')"
|
||||
:header-text="s__('Pipelines|Inputs')"
|
||||
:search-placeholder="s__('Pipelines|Search input name')"
|
||||
:show-select-all-button-label="__('Select all')"
|
||||
:reset-button-label="__('Clear')"
|
||||
:disabled="!hasInputs"
|
||||
searchable
|
||||
|
|
@ -286,6 +291,7 @@ export default {
|
|||
size="small"
|
||||
@reset="deselectAll"
|
||||
@select="selectInputs"
|
||||
@select-all="selectAll"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -52,12 +52,12 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="build-job gl-relative" :class="classes">
|
||||
<div class="build-job gl-relative gl-px-2" :class="classes">
|
||||
<gl-link
|
||||
v-gl-tooltip.left.viewport
|
||||
:href="job.status.details_path"
|
||||
:title="tooltipText"
|
||||
class="gl-flex gl-items-center gl-py-3 gl-pl-7"
|
||||
class="gl-mb-1 gl-flex gl-items-center gl-py-2 gl-pl-7"
|
||||
:data-testid="dataTestId"
|
||||
>
|
||||
<gl-icon
|
||||
|
|
@ -67,7 +67,7 @@ export default {
|
|||
class="icon-arrow-right gl-absolute gl-block"
|
||||
/>
|
||||
|
||||
<ci-icon :status="job.status" :show-tooltip="false" class="gl-mr-3" />
|
||||
<ci-icon tabindex="-1" :status="job.status" :show-tooltip="false" class="gl-mr-3" />
|
||||
|
||||
<span class="gl-w-full gl-truncate">{{ jobName }}</span>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
<script>
|
||||
import { GlTooltip, GlDisclosureDropdown } from '@gitlab/ui';
|
||||
import { GlTooltip, GlDisclosureDropdown, GlBadge } from '@gitlab/ui';
|
||||
import { uniqueId } from 'lodash';
|
||||
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDisclosureDropdown,
|
||||
GlTooltip,
|
||||
GlBadge,
|
||||
},
|
||||
inject: ['tiptapEditor', 'contentEditor'],
|
||||
data() {
|
||||
|
|
@ -45,6 +46,17 @@ export default {
|
|||
text: __('Horizontal rule'),
|
||||
action: () => this.execute('setHorizontalRule', 'horizontalRule'),
|
||||
},
|
||||
{
|
||||
text: __('GitLab Query Language (GLQL) view'),
|
||||
action: () => this.execute('insertGLQLView', 'glqlView'),
|
||||
badge: {
|
||||
text: __('Beta'),
|
||||
variant: 'info',
|
||||
size: 'small',
|
||||
target: '_blank',
|
||||
href: helpPagePath('user/glql/_index'),
|
||||
},
|
||||
},
|
||||
{
|
||||
text: __('Mermaid diagram'),
|
||||
action: () => this.insert('diagram', { language: 'mermaid' }),
|
||||
|
|
@ -109,7 +121,16 @@ export default {
|
|||
:toggle-text="__('More options')"
|
||||
text-sr-only
|
||||
right
|
||||
/>
|
||||
>
|
||||
<template #list-item="{ item }">
|
||||
<span class="gl-flex gl-items-center gl-justify-between">
|
||||
{{ item.text }}
|
||||
<gl-badge v-if="item.badge" v-bind="item.badge" class="gl-ml-4" @click.stop>
|
||||
{{ item.badge.text }}
|
||||
</gl-badge>
|
||||
</span>
|
||||
</template>
|
||||
</gl-disclosure-dropdown>
|
||||
<gl-tooltip :target="toggleId" placement="top">{{ __('More options') }}</gl-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
|
|||
import { Fragment } from '@tiptap/pm/model';
|
||||
import { mergeAttributes, textblockTypeInputRule } from '@tiptap/core';
|
||||
import { VueNodeViewRenderer } from '@tiptap/vue-2';
|
||||
import { __ } from '~/locale';
|
||||
import languageLoader from '../services/code_block_language_loader';
|
||||
import CodeBlockWrapper from '../components/wrappers/code_block.vue';
|
||||
|
||||
const DEFAULT_GLQL_VIEW_CONTENT = `query: assignee = currentUser()\nfields: title, createdAt, milestone, assignee\ntitle: ${__('Issues assigned to current user')}`;
|
||||
|
||||
const extractLanguage = (element) => element.dataset.canonicalLang ?? element.getAttribute('lang');
|
||||
|
||||
export const backtickInputRegex = /^```([a-z]+)?[\s\n]$/;
|
||||
|
|
@ -56,6 +59,27 @@ export default CodeBlockLowlight.extend({
|
|||
}),
|
||||
];
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
insertGLQLView:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.insertContent({
|
||||
type: this.type.name,
|
||||
attrs: {
|
||||
language: 'glql',
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: DEFAULT_GLQL_VIEW_CONTENT,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -60,6 +60,11 @@ export default {
|
|||
required: false,
|
||||
default: true,
|
||||
},
|
||||
hideOnNarrowScreen: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
expandButtonInfo() {
|
||||
|
|
@ -101,14 +106,15 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-hidden gl-items-center md:gl-flex">
|
||||
<div class="gl-items-center" :class="hideOnNarrowScreen ? 'gl-hidden md:gl-flex' : 'gl-flex'">
|
||||
<template v-if="hasChanges">
|
||||
<diff-stats
|
||||
v-if="diffsCount !== ''"
|
||||
class="inline-parallel-buttons ml-auto gl-hidden md:gl-flex"
|
||||
class="inline-parallel-buttons ml-auto"
|
||||
:diffs-count="diffsCount"
|
||||
:added-lines="addedLines"
|
||||
:removed-lines="removedLines"
|
||||
:hide-on-narrow-screen="hideOnNarrowScreen"
|
||||
/>
|
||||
<gl-button-group class="gl-mr-3">
|
||||
<gl-button
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
hideOnNarrowScreen: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
diffFilesLength() {
|
||||
|
|
@ -65,8 +70,10 @@ export default {
|
|||
<div
|
||||
class="diff-stats"
|
||||
:class="{
|
||||
'is-compare-versions-header gl-hidden lg:gl-inline-flex': isCompareVersionsHeader,
|
||||
'gl-hidden sm:!gl-inline-flex': !isCompareVersionsHeader,
|
||||
'is-compare-versions-header gl-hidden lg:gl-inline-flex':
|
||||
isCompareVersionsHeader && hideOnNarrowScreen,
|
||||
'gl-hidden sm:!gl-inline-flex': !isCompareVersionsHeader && hideOnNarrowScreen,
|
||||
'gl-inline-flex': !hideOnNarrowScreen,
|
||||
}"
|
||||
>
|
||||
<div v-if="notDiffable" :class="fileStats.classes">
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export default {
|
|||
<gl-button
|
||||
v-gl-tooltip.html="toggleFileBrowserTooltip"
|
||||
variant="default"
|
||||
class="js-toggle-tree-list btn-icon gl-mr-3"
|
||||
class="btn-icon gl-mr-3 max-lg:gl-hidden"
|
||||
data-testid="file-tree-button"
|
||||
:aria-label="toggleFileBrowserTitle"
|
||||
:aria-keyshortcuts="toggleFileBrowserShortcutKey"
|
||||
|
|
|
|||
|
|
@ -6,12 +6,19 @@ export const useFileBrowser = defineStore('fileBrowser', {
|
|||
state() {
|
||||
return {
|
||||
fileBrowserVisible: true,
|
||||
fileBrowserDrawerVisible: false,
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
setFileBrowserVisibility(visible) {
|
||||
this.fileBrowserVisible = visible;
|
||||
},
|
||||
setFileBrowserDrawerVisibility(visible) {
|
||||
this.fileBrowserDrawerVisible = visible;
|
||||
},
|
||||
toggleFileBrowserDrawerVisibility() {
|
||||
this.fileBrowserDrawerVisible = !this.fileBrowserDrawerVisible;
|
||||
},
|
||||
toggleFileBrowserVisibility() {
|
||||
this.fileBrowserVisible = !this.fileBrowserVisible;
|
||||
setCookie(FILE_BROWSER_VISIBLE, this.fileBrowserVisible);
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export function removeListenerSystemColorSchemeChange(onEvent) {
|
|||
.removeEventListener('change', (event) => handleColorSchemeChange(onEvent, event));
|
||||
}
|
||||
|
||||
function isNarrowScreenMediaQuery() {
|
||||
export function isNarrowScreenMediaQuery() {
|
||||
const computedStyles = getComputedStyle(document.body);
|
||||
const largeBreakpointSize = parseInt(computedStyles.getPropertyValue('--breakpoint-lg'), 10);
|
||||
return window.matchMedia(`(max-width: ${largeBreakpointSize - 1}px)`);
|
||||
|
|
|
|||
|
|
@ -337,6 +337,7 @@ export default class MergeRequestTabs {
|
|||
}
|
||||
|
||||
this.expandSidebar?.forEach((el) => el.classList.toggle('!gl-hidden', action !== 'show'));
|
||||
this.rapidDiffsApp?.hide?.();
|
||||
|
||||
if (action === 'commits') {
|
||||
if (!this.commitsLoaded) {
|
||||
|
|
@ -537,6 +538,8 @@ export default class MergeRequestTabs {
|
|||
this.rapidDiffsApp = this.createRapidDiffsApp();
|
||||
this.rapidDiffsApp.reloadDiffs(true);
|
||||
this.rapidDiffsApp.init();
|
||||
} else {
|
||||
this.rapidDiffsApp.show();
|
||||
}
|
||||
} else {
|
||||
this.loadDiff(options);
|
||||
|
|
|
|||
|
|
@ -141,6 +141,8 @@ export default {
|
|||
containerRepositoriesCount: 0,
|
||||
itemToDelete: {},
|
||||
deleteAlertType: null,
|
||||
deleteAlertMessage: null,
|
||||
deleteImageErrorMessages: [],
|
||||
sorting: null,
|
||||
name: null,
|
||||
mutationLoading: false,
|
||||
|
|
@ -197,11 +199,6 @@ export default {
|
|||
showConnectionError() {
|
||||
return this.config.connectionError || this.config.invalidPathError;
|
||||
},
|
||||
deleteImageAlertMessage() {
|
||||
return this.deleteAlertType === 'success'
|
||||
? DELETE_IMAGE_SUCCESS_MESSAGE
|
||||
: DELETE_IMAGE_ERROR_MESSAGE;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
deleteImage(item) {
|
||||
|
|
@ -210,7 +207,8 @@ export default {
|
|||
this.$refs.deleteModal.show();
|
||||
},
|
||||
dismissDeleteAlert() {
|
||||
this.deleteAlertType = null;
|
||||
this.setDeleteAlert(null, null);
|
||||
|
||||
this.itemToDelete = {};
|
||||
},
|
||||
fetchNextPage() {
|
||||
|
|
@ -239,6 +237,20 @@ export default {
|
|||
}, 200);
|
||||
}
|
||||
},
|
||||
setDeleteAlert(alertType, alertMessage) {
|
||||
this.deleteAlertType = alertType;
|
||||
this.deleteAlertMessage = alertMessage;
|
||||
},
|
||||
setDeleteErrorMessages(deleteErrorMessages = []) {
|
||||
this.deleteImageErrorMessages = deleteErrorMessages ?? [];
|
||||
},
|
||||
handleDeleteImageSuccess() {
|
||||
this.setDeleteAlert('success', DELETE_IMAGE_SUCCESS_MESSAGE);
|
||||
},
|
||||
handleDeleteImageError(errors = []) {
|
||||
this.setDeleteAlert('danger', DELETE_IMAGE_ERROR_MESSAGE);
|
||||
this.setDeleteErrorMessages(errors?.map(({ message }) => message));
|
||||
},
|
||||
},
|
||||
containerRegistryHelpUrl: helpPagePath('user/packages/container_registry/_index'),
|
||||
dockerConnectionErrorHelpUrl: helpPagePath(
|
||||
|
|
@ -260,11 +272,19 @@ export default {
|
|||
dismissible
|
||||
@dismiss="dismissDeleteAlert"
|
||||
>
|
||||
<gl-sprintf :message="deleteImageAlertMessage">
|
||||
<gl-sprintf :message="deleteAlertMessage">
|
||||
<template #title>
|
||||
{{ itemToDelete.path }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
|
||||
<div v-if="deleteImageErrorMessages.length">
|
||||
<ul>
|
||||
<li v-for="(deleteImageErrorMessage, index) in deleteImageErrorMessages" :key="index">
|
||||
{{ deleteImageErrorMessage }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</gl-alert>
|
||||
|
||||
<gl-empty-state
|
||||
|
|
@ -374,8 +394,8 @@ export default {
|
|||
<delete-image
|
||||
:id="itemToDelete.id"
|
||||
@start="startDelete"
|
||||
@error="deleteAlertType = 'danger'"
|
||||
@success="deleteAlertType = 'success'"
|
||||
@error="handleDeleteImageError"
|
||||
@success="handleDeleteImageSuccess"
|
||||
@end="mutationLoading = false"
|
||||
>
|
||||
<template #default="{ doDelete }">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { isNarrowScreenMediaQuery } from '~/lib/utils/css_utils';
|
||||
|
||||
export const useViewport = defineStore('viewportStore', () => {
|
||||
const isNarrowScreen = ref(false);
|
||||
|
||||
const updateIsNarrow = (matches) => {
|
||||
isNarrowScreen.value = matches;
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
isNarrowScreen.value = false;
|
||||
};
|
||||
|
||||
const query = isNarrowScreenMediaQuery();
|
||||
updateIsNarrow(query.matches);
|
||||
|
||||
query.addEventListener('change', (event) => {
|
||||
updateIsNarrow(event.matches);
|
||||
});
|
||||
|
||||
return {
|
||||
// used only for testing
|
||||
updateIsNarrow,
|
||||
// used only for testing
|
||||
reset,
|
||||
isNarrowScreen: computed(() => isNarrowScreen.value),
|
||||
};
|
||||
});
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<script>
|
||||
import { mapState } from 'pinia';
|
||||
import { MountingPortal } from 'portal-vue';
|
||||
import { GlDrawer } from '@gitlab/ui';
|
||||
import DiffsFileTree from '~/diffs/components/diffs_file_tree.vue';
|
||||
import { useDiffsList } from '~/rapid_diffs/stores/diffs_list';
|
||||
import { useFileBrowser } from '~/diffs/stores/file_browser';
|
||||
import { useDiffsView } from '~/rapid_diffs/stores/diffs_view';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
|
||||
|
||||
export default {
|
||||
name: 'FileBrowserDrawer',
|
||||
DRAWER_Z_INDEX,
|
||||
components: {
|
||||
MountingPortal,
|
||||
DiffsFileTree,
|
||||
GlDrawer,
|
||||
},
|
||||
props: {
|
||||
groupBlobsListItems: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openedOnce: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useDiffsView, ['totalFilesCount']),
|
||||
...mapState(useDiffsList, ['loadedFiles']),
|
||||
...mapState(useFileBrowser, ['fileBrowserDrawerVisible']),
|
||||
},
|
||||
watch: {
|
||||
fileBrowserDrawerVisible() {
|
||||
this.openedOnce = true;
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
useFileBrowser().setFileBrowserDrawerVisibility(false);
|
||||
},
|
||||
methods: {
|
||||
clickFile(file) {
|
||||
this.$emit('clickFile', file);
|
||||
this.close();
|
||||
},
|
||||
toggleFolder(path) {
|
||||
useLegacyDiffs().toggleTreeOpen(path);
|
||||
},
|
||||
close() {
|
||||
useFileBrowser().setFileBrowserDrawerVisibility(false);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<mounting-portal append mount-to="#js-drawer-container">
|
||||
<gl-drawer
|
||||
v-show="fileBrowserDrawerVisible"
|
||||
:open="openedOnce"
|
||||
:z-index="$options.DRAWER_Z_INDEX"
|
||||
@close="close"
|
||||
>
|
||||
<template #title>
|
||||
<h2 class="gl-my-0 gl-text-size-h2 gl-leading-24">{{ s__('RapidDiffs|File browser') }}</h2>
|
||||
</template>
|
||||
<template #default>
|
||||
<diffs-file-tree
|
||||
class="diffs-tree-drawer"
|
||||
:loaded-files="loadedFiles"
|
||||
:total-files-count="totalFilesCount"
|
||||
:group-blobs-list-items="groupBlobsListItems"
|
||||
@clickFile="clickFile"
|
||||
@toggleFolder="toggleFolder"
|
||||
/>
|
||||
</template>
|
||||
</gl-drawer>
|
||||
</mounting-portal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<script>
|
||||
import { GlAnimatedSidebarIcon, GlButton } from '@gitlab/ui';
|
||||
import { mapActions, mapState } from 'pinia';
|
||||
import { __ } from '~/locale';
|
||||
import { useFileBrowser } from '~/diffs/stores/file_browser';
|
||||
|
||||
export default {
|
||||
name: 'FileBrowserDrawerToggle',
|
||||
components: {
|
||||
GlButton,
|
||||
GlAnimatedSidebarIcon,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useFileBrowser, ['fileBrowserDrawerVisible']),
|
||||
toggleFileBrowserTitle() {
|
||||
return this.fileBrowserDrawerVisible ? __('Hide file browser') : __('Show file browser');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useFileBrowser, ['toggleFileBrowserDrawerVisibility']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-button
|
||||
variant="default"
|
||||
category="tertiary"
|
||||
class="btn-icon -gl-mr-3 -gl-scale-x-100 lg:gl-hidden"
|
||||
:aria-label="toggleFileBrowserTitle"
|
||||
data-testid="file-tree-drawer-button"
|
||||
@click="toggleFileBrowserDrawerVisibility"
|
||||
>
|
||||
<gl-animated-sidebar-icon :is-on="fileBrowserDrawerVisible" class="gl-button-icon" />
|
||||
</gl-button>
|
||||
</template>
|
||||
|
|
@ -14,6 +14,7 @@ import { DIFF_FILE_MOUNTED } from '~/rapid_diffs/dom_events';
|
|||
import { VIEWER_ADAPTERS } from '~/rapid_diffs/adapters';
|
||||
import { camelizeKeys } from '~/lib/utils/object_utils';
|
||||
import { disableBrokenContentVisibility } from '~/rapid_diffs/app/content_visibility_fix';
|
||||
import { useApp } from '~/rapid_diffs/stores/app';
|
||||
|
||||
// This facade interface joins together all the bits and pieces of Rapid Diffs: DiffFile, Settings, File browser, etc.
|
||||
// It's a unified entrypoint for Rapid Diffs and all external communications should happen through this interface.
|
||||
|
|
@ -51,6 +52,16 @@ export class RapidDiffsFacade {
|
|||
this.intersectionObserver.unobserve(instance);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
show() {
|
||||
useApp().appVisible = true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
hide() {
|
||||
useApp().appVisible = false;
|
||||
}
|
||||
|
||||
#delegateEvents() {
|
||||
this.root.addEventListener('click', (event) => {
|
||||
const diffFile = event.target.closest('diff-file');
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import { generateTreeList } from '~/diffs/utils/tree_worker_utils';
|
|||
import { SET_TREE_DATA } from '~/diffs/store/mutation_types';
|
||||
import { linkTreeNodes, sortTree } from '~/ide/stores/utils';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { useViewport } from '~/pinia/global_stores/viewport';
|
||||
import { useApp } from '~/rapid_diffs/stores/app';
|
||||
import FileBrowser from './file_browser.vue';
|
||||
import FileBrowserDrawer from './file_browser_drawer.vue';
|
||||
import FileBrowserDrawerToggle from './file_browser_drawer_toggle.vue';
|
||||
|
||||
const loadFileBrowserData = async (diffFilesEndpoint, shouldSort) => {
|
||||
const { data } = await axios.get(diffFilesEndpoint);
|
||||
|
|
@ -19,11 +23,33 @@ const loadFileBrowserData = async (diffFilesEndpoint, shouldSort) => {
|
|||
};
|
||||
|
||||
const initToggle = (el) => {
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: document.querySelector('#js-page-breadcrumbs-extra'),
|
||||
pinia,
|
||||
computed: {
|
||||
visible() {
|
||||
return useViewport().isNarrowScreen && useApp().appVisible;
|
||||
},
|
||||
},
|
||||
render(h) {
|
||||
if (!this.visible) return null;
|
||||
|
||||
return h(FileBrowserDrawerToggle);
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
pinia,
|
||||
computed: {
|
||||
visible() {
|
||||
return !useViewport().isNarrowScreen;
|
||||
},
|
||||
},
|
||||
render(h) {
|
||||
if (!this.visible) return null;
|
||||
|
||||
return h(FileBrowserToggle);
|
||||
},
|
||||
});
|
||||
|
|
@ -35,7 +61,7 @@ const initBrowserComponent = async (el, shouldSort) => {
|
|||
el,
|
||||
pinia,
|
||||
render(h) {
|
||||
return h(FileBrowser, {
|
||||
return h(useViewport().isNarrowScreen ? FileBrowserDrawer : FileBrowser, {
|
||||
props: {
|
||||
groupBlobsListItems: shouldSort,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ const initSettingsApp = (el, pinia) => {
|
|||
addedLines: this.diffsStats?.addedLines,
|
||||
removedLines: this.diffsStats?.removedLines,
|
||||
diffsCount: this.diffsStats?.diffsCount,
|
||||
hideOnNarrowScreen: false,
|
||||
},
|
||||
on: {
|
||||
updateDiffViewType: this.updateViewType,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useApp = defineStore('rapidDiffsApp', {
|
||||
state() {
|
||||
return {
|
||||
appVisible: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -105,6 +105,7 @@ export default {
|
|||
:content="blobContent"
|
||||
:active-viewer="viewer"
|
||||
:blob="blob"
|
||||
:is-snippet="true"
|
||||
@[$options.BLOB_RENDER_EVENT_LOAD]="forceQuery"
|
||||
@[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="switchViewer"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,13 @@ export default {
|
|||
SafeHtml,
|
||||
},
|
||||
mixins: [ViewerMixin],
|
||||
props: {
|
||||
isSnippet: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: true,
|
||||
|
|
@ -34,6 +41,13 @@ export default {
|
|||
isMarkup() {
|
||||
return this.type === MARKUP_FILE_TYPE;
|
||||
},
|
||||
forbiddenAttrs() {
|
||||
const attrs = [...defaultConfig.FORBID_ATTR, 'data-lines-path'];
|
||||
if (this.isSnippet) {
|
||||
attrs.push('style');
|
||||
}
|
||||
return attrs;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.optimizeMarkupRendering();
|
||||
|
|
@ -55,7 +69,10 @@ export default {
|
|||
if (!this.isMarkup) return;
|
||||
|
||||
const tmpWrapper = document.createElement('div');
|
||||
tmpWrapper.innerHTML = sanitize(this.rawContent, this.$options.safeHtmlConfig);
|
||||
tmpWrapper.innerHTML = sanitize(this.rawContent, {
|
||||
...defaultConfig,
|
||||
FORBID_ATTR: this.forbiddenAttrs,
|
||||
});
|
||||
|
||||
const fileContent = tmpWrapper.querySelector(MARKUP_CONTENT_SELECTOR);
|
||||
if (!fileContent) return;
|
||||
|
|
@ -99,7 +116,7 @@ export default {
|
|||
},
|
||||
safeHtmlConfig: {
|
||||
...defaultConfig,
|
||||
FORBID_ATTR: [...defaultConfig.FORBID_ATTR, 'style', 'data-lines-path'],
|
||||
FORBID_ATTR: [...defaultConfig.FORBID_ATTR, 'data-lines-path'],
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,13 @@ import CommentTemplatesModal from './comment_templates_modal.vue';
|
|||
import HeaderDivider from './header_divider.vue';
|
||||
|
||||
export default {
|
||||
findAndReplace: {
|
||||
highlightColor: '#fdf1dd',
|
||||
highlightColorActive: '#e6e4f2',
|
||||
highlightClass: 'js-highlight',
|
||||
highlightClassActive: 'js-highlight-active',
|
||||
},
|
||||
|
||||
components: {
|
||||
ToolbarButton,
|
||||
ToolbarTableButton,
|
||||
|
|
@ -174,6 +181,26 @@ export default {
|
|||
showSuggestPopover() {
|
||||
this.updateSuggestPopoverVisibility();
|
||||
},
|
||||
'findAndReplace.highlightedMatchIndex': {
|
||||
handler(newValue) {
|
||||
const options = this.$options.findAndReplace;
|
||||
const previousActive = this.cloneDiv.querySelector(`.${options.highlightClassActive}`);
|
||||
|
||||
if (previousActive) {
|
||||
previousActive.classList.remove(options.highlightClassActive);
|
||||
previousActive.style.backgroundColor = options.highlightColor;
|
||||
}
|
||||
|
||||
const newActive = this.cloneDiv
|
||||
.querySelectorAll(`.${options.highlightClass}`)
|
||||
.item(newValue - 1);
|
||||
|
||||
if (newActive) {
|
||||
newActive.classList.add(options.highlightClassActive);
|
||||
newActive.style.backgroundColor = options.highlightColorActive;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
$(document).on('markdown-preview:show.vue', this.showMarkdownPreview);
|
||||
|
|
@ -306,7 +333,11 @@ export default {
|
|||
}
|
||||
},
|
||||
findAndReplace_handleKeyUp(e) {
|
||||
this.findAndReplace_highlightMatchingText(e.target.value);
|
||||
if (e.key === 'Enter') {
|
||||
this.findAndReplace_handleNext();
|
||||
} else {
|
||||
this.findAndReplace_highlightMatchingText(e.target.value);
|
||||
}
|
||||
},
|
||||
findAndReplace_syncScroll() {
|
||||
const textArea = this.getCurrentTextArea();
|
||||
|
|
@ -322,20 +353,30 @@ export default {
|
|||
|
||||
const regex = new RegExp(`(${textToFind})`, 'g');
|
||||
const segments = textArea.value.split(regex);
|
||||
const options = this.$options.findAndReplace;
|
||||
|
||||
// Clear previous contents
|
||||
this.cloneDiv.innerHTML = '';
|
||||
let counter = 0;
|
||||
|
||||
segments.forEach((segment) => {
|
||||
// If the segment matches the text we're highlighting
|
||||
if (segment === textToFind) {
|
||||
const span = document.createElement('span');
|
||||
span.classList.add('js-highlight');
|
||||
span.style.backgroundColor = 'orange';
|
||||
span.classList.add(options.highlightClass);
|
||||
span.style.backgroundColor = options.highlightColor;
|
||||
span.style.display = 'inline-block';
|
||||
span.textContent = segment; // Use textContent for safe text insertion
|
||||
|
||||
// Highlight first match
|
||||
if (counter === 0) {
|
||||
span.classList.add(options.highlightClassActive);
|
||||
span.style.backgroundColor = options.highlightColorActive;
|
||||
}
|
||||
|
||||
this.cloneDiv.appendChild(span);
|
||||
this.findAndReplace.totalMatchCount += 1;
|
||||
counter += 1;
|
||||
} else {
|
||||
// Otherwise, just append the plain text
|
||||
const textNode = document.createTextNode(segment);
|
||||
|
|
@ -409,6 +450,20 @@ export default {
|
|||
// Required to align the clone div
|
||||
this.cloneDiv.scrollTop = textArea.scrollTop;
|
||||
},
|
||||
findAndReplace_handlePrev() {
|
||||
this.findAndReplace.highlightedMatchIndex -= 1;
|
||||
|
||||
if (this.findAndReplace.highlightedMatchIndex <= 0) {
|
||||
this.findAndReplace.highlightedMatchIndex = this.findAndReplace.totalMatchCount;
|
||||
}
|
||||
},
|
||||
findAndReplace_handleNext() {
|
||||
this.findAndReplace.highlightedMatchIndex += 1;
|
||||
|
||||
if (this.findAndReplace.highlightedMatchIndex > this.findAndReplace.totalMatchCount) {
|
||||
this.findAndReplace.highlightedMatchIndex = 1;
|
||||
}
|
||||
},
|
||||
},
|
||||
shortcuts: {
|
||||
bold: keysFor(BOLD_TEXT),
|
||||
|
|
@ -739,6 +794,24 @@ export default {
|
|||
<div class="gl-ml-4 gl-min-w-12 gl-whitespace-nowrap">
|
||||
{{ findAndReplace_MatchCountText }}
|
||||
</div>
|
||||
<div class="gl-ml-2 gl-flex gl-items-center">
|
||||
<gl-button
|
||||
category="tertiary"
|
||||
icon="arrow-up"
|
||||
size="small"
|
||||
data-testid="find-prev"
|
||||
:aria-label="s__('MarkdownEditor|Find previous')"
|
||||
@click="findAndReplace_handlePrev"
|
||||
/>
|
||||
<gl-button
|
||||
category="tertiary"
|
||||
icon="arrow-down"
|
||||
size="small"
|
||||
data-testid="find-next"
|
||||
:aria-label="s__('MarkdownEditor|Find next')"
|
||||
@click="findAndReplace_handleNext"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -84,8 +84,15 @@
|
|||
// override .diff-tree-list, remove when fully switched to Rapid Diffs
|
||||
width: auto !important;
|
||||
position: static !important;
|
||||
padding: 0 0 $gl-spacing-scale-4 !important;
|
||||
min-height: auto !important;
|
||||
padding: $gl-spacing-scale-4 !important;
|
||||
min-height: 0 !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.rd-app .rd-app-sidebar {
|
||||
@include media-breakpoint-down(constants.$app-vertical-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -454,6 +454,10 @@ span.idiff {
|
|||
}
|
||||
}
|
||||
|
||||
.diffs-tree-drawer .mr-tree-list {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.mr-tree-list:not(.tree-list-blobs) .tree-list-parent::before, .repository-tree-list .tree-list-parent::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -108,8 +108,7 @@
|
|||
overflow: auto;
|
||||
|
||||
.icon-arrow-right {
|
||||
left: 8px;
|
||||
top: 12px;
|
||||
@apply gl-top-3 gl-left-3;
|
||||
}
|
||||
|
||||
.build-job {
|
||||
|
|
@ -120,6 +119,10 @@
|
|||
&:hover {
|
||||
@apply gl-bg-strong;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
@apply gl-relative gl-z-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ module Types
|
|||
experiment: { milestone: '15.7' }
|
||||
value 'EPIC', value: 'epic',
|
||||
description: 'Epic issue type. ' \
|
||||
'Available only when feature epics is available and the feature flag `work_item_epics` is enabled.',
|
||||
'Available only when feature epics is available.',
|
||||
experiment: { milestone: '16.7' }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ module NavHelper
|
|||
end
|
||||
|
||||
def work_item_epic_page?
|
||||
current_controller?('epics') && @group.work_item_epics_enabled?
|
||||
current_controller?('epics')
|
||||
end
|
||||
|
||||
def new_issue_look?
|
||||
|
|
|
|||
|
|
@ -1125,7 +1125,7 @@ class Group < Namespace
|
|||
end
|
||||
|
||||
def create_group_level_work_items_feature_flag_enabled?
|
||||
::Feature.enabled?(:create_group_level_work_items, self, type: :wip)
|
||||
::Feature.enabled?(:create_group_level_work_items, self, type: :wip) && supports_group_work_items?
|
||||
end
|
||||
|
||||
def supports_lock_on_merge?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "vulnerability_export#report_data",
|
||||
"description": "Schema for providing additional report data for vulnerability report exports",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"project_vulnerabilities_history": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"description": "This object should include an SVG asset to be rendered in PDF reports"
|
||||
},
|
||||
"group_vulnerabilities_history": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"description": "This object should include an SVG asset to be rendered in PDF reports"
|
||||
},
|
||||
"project_security_status": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"description": "Data for rendering the project grades summary in PDF reports"
|
||||
},
|
||||
"dashboard_type": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"enum": [
|
||||
"product",
|
||||
"group",
|
||||
"dashboard",
|
||||
null
|
||||
],
|
||||
"description": "The type of vulnerability grouping this report is being created for"
|
||||
},
|
||||
"full_path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "The full path of the vulnerable"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@
|
|||
%body{ class: body_classes, data: body_data }
|
||||
-# all tooltips from GitLab UI will mount here by default
|
||||
#js-tooltips-container
|
||||
-# mount drawers here for better page performance
|
||||
#js-drawer-container
|
||||
= render "layouts/init_auto_complete" if @gfm_form
|
||||
= render "layouts/init_client_detection_flags"
|
||||
= render 'peek/bar'
|
||||
|
|
|
|||
|
|
@ -13,5 +13,6 @@
|
|||
#js-vue-page-breadcrumbs-wrapper{ data: { testid: 'breadcrumb-links' } }
|
||||
#js-vue-page-breadcrumbs{ data: { breadcrumbs_json: breadcrumbs_as_json } }
|
||||
#js-injected-page-breadcrumbs
|
||||
#js-page-breadcrumbs-extra
|
||||
|
||||
= yield :header_content
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
- add_page_specific_style 'page_bundles/branches'
|
||||
- page_title _('Branches')
|
||||
- title = _('Branches')
|
||||
- page_title title
|
||||
- search = params[:search]
|
||||
|
||||
-# Possible values for variables passed down from the projects/branches_controller.rb
|
||||
|
|
@ -7,6 +8,8 @@
|
|||
-# @mode - overview|active|stale|all (default:overview)
|
||||
-# @sort - name_asc|updated_asc|updated_desc
|
||||
|
||||
%h1.gl-sr-only= title
|
||||
|
||||
.top-area
|
||||
= gl_tabs_nav({ class: 'gl-grow gl-border-b-0' }) do
|
||||
= gl_tab_link_to s_('Branches|Overview'), project_branches_path(@project), { item_active: @mode == 'overview', title: s_('Branches|Show overview of the branches') }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
- add_page_specific_style 'page_bundles/projects'
|
||||
- page_title _("Commits"), @ref
|
||||
|
||||
%h1.gl-sr-only= page_title
|
||||
|
||||
= content_for :meta_tags do
|
||||
= auto_discovery_link_tag(:atom, project_commits_path(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
- page_title s_("Snippets|Snippets")
|
||||
- new_project_snippet_link = new_project_snippet_path(@project) if can?(current_user, :create_snippet, @project)
|
||||
|
||||
%h1.gl-sr-only= page_title
|
||||
|
||||
- if @snippets.exists?
|
||||
- if current_user
|
||||
.top-area
|
||||
|
|
|
|||
|
|
@ -8,14 +8,6 @@ description: The SHA referencing changes to a single design or multiple design f
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/10552
|
||||
milestone: '11.10'
|
||||
gitlab_schema: gitlab_main_cell
|
||||
desired_sharding_key:
|
||||
namespace_id:
|
||||
references: namespaces
|
||||
backfill_via:
|
||||
parent:
|
||||
foreign_key: design_id
|
||||
table: design_management_designs
|
||||
sharding_key: namespace_id
|
||||
belongs_to: design
|
||||
sharding_key:
|
||||
namespace_id: namespaces
|
||||
table_size: small
|
||||
desired_sharding_key_migration_job_name: BackfillDesignManagementDesignsVersionsNamespaceId
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDesignManagementDesignsVersionsNamespaceIdNotNull < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.1'
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_not_null_constraint :design_management_designs_versions, :namespace_id
|
||||
end
|
||||
|
||||
def down
|
||||
remove_not_null_constraint :design_management_designs_versions, :namespace_id
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
9eb96106e37385188350578a21fec79cf8a5ddd69e76aa5dfe807d8a3a8b8145
|
||||
|
|
@ -13703,7 +13703,8 @@ CREATE TABLE design_management_designs_versions (
|
|||
version_id bigint NOT NULL,
|
||||
event smallint DEFAULT 0 NOT NULL,
|
||||
image_v432x230 character varying(255),
|
||||
namespace_id bigint
|
||||
namespace_id bigint,
|
||||
CONSTRAINT check_ae7359f44b CHECK ((namespace_id IS NOT NULL))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE design_management_designs_versions_id_seq
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ For more examples of a question you can ask, see
|
|||
| Feature | Available on GitLab Duo Self-Hosted | GitLab version | Status |
|
||||
| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ---------------------- | --- |
|
||||
| [GitLab Duo for the CLI](../../editor_extensions/gitlab_cli/_index.md#gitlab-duo-for-the-cli) | {{< icon name="check-circle-filled" >}} Yes | GitLab 18.1 and later | Beta |
|
||||
| [GitLab Duo Workflow](../../user/duo_workflow/_index.md) | {{< icon name="dash-circle" >}} No | GitLab 17.4 and later | Beta |
|
||||
| [GitLab Duo Workflow](../../user/duo_workflow/_index.md) | {{< icon name="dash-circle" >}} No | GitLab 17.4 and later | Not applicable |
|
||||
| [Vulnerability Resolution](../../user/application_security/vulnerabilities/_index.md#vulnerability-resolution) | {{< icon name="check-circle-filled" >}} Yes | GitLab 18.1 and later | Beta |
|
||||
| [AI Impact Dashboard](../../user/analytics/ai_impact_analytics.md) | {{< icon name="check-circle-filled" >}} Yes | GitLab 17.9 and later | Beta |
|
||||
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ When restricting visibility levels, consider how these restrictions interact
|
|||
with permissions for subgroups and projects that inherit their visibility from
|
||||
the item you're changing.
|
||||
|
||||
This setting does not apply to groups and projects created under a personal namespace.
|
||||
This setting does not apply to projects created under a personal namespace.
|
||||
There is a [feature request](https://gitlab.com/gitlab-org/gitlab/-/issues/382749) to extend this
|
||||
functionality to [enterprise users](../../user/enterprise_user/_index.md).
|
||||
|
||||
|
|
|
|||
|
|
@ -7868,6 +7868,35 @@ Input type: `LdapAdminRoleLinkDestroyInput`
|
|||
| <a id="mutationldapadminrolelinkdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during the mutation. |
|
||||
| <a id="mutationldapadminrolelinkdestroyldapadminrolelink"></a>`ldapAdminRoleLink` | [`LdapAdminRoleLink`](#ldapadminrolelink) | Deleted instance-level LDAP link. |
|
||||
|
||||
### `Mutation.lifecycleUpdate`
|
||||
|
||||
{{< details >}}
|
||||
**Introduced** in GitLab 18.1.
|
||||
**Status**: Experiment.
|
||||
{{< /details >}}
|
||||
|
||||
Input type: `LifecycleUpdateInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationlifecycleupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationlifecycleupdatedefaultclosedstatusindex"></a>`defaultClosedStatusIndex` | [`Int`](#int) | Index of the default closed status in the statuses array. |
|
||||
| <a id="mutationlifecycleupdatedefaultduplicatestatusindex"></a>`defaultDuplicateStatusIndex` | [`Int`](#int) | Index of the default duplicated status in the statuses array. |
|
||||
| <a id="mutationlifecycleupdatedefaultopenstatusindex"></a>`defaultOpenStatusIndex` | [`Int`](#int) | Index of the default open status in the statuses array. |
|
||||
| <a id="mutationlifecycleupdateid"></a>`id` | [`WorkItemsStatusesLifecycleID!`](#workitemsstatuseslifecycleid) | Global ID of the lifecycle to be updated. |
|
||||
| <a id="mutationlifecycleupdatenamespacepath"></a>`namespacePath` | [`ID!`](#id) | Namespace path where the lifecycle exists. |
|
||||
| <a id="mutationlifecycleupdatestatuses"></a>`statuses` | [`[WorkItemStatusInput!]`](#workitemstatusinput) | Statuses of the lifecycle. Can be existing (with id) or new (without id). |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationlifecycleupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationlifecycleupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during the mutation. |
|
||||
| <a id="mutationlifecycleupdatelifecycle"></a>`lifecycle` | [`WorkItemLifecycle`](#workitemlifecycle) | Lifecycle updated. |
|
||||
|
||||
### `Mutation.markAsSpamSnippet`
|
||||
|
||||
Input type: `MarkAsSpamSnippetInput`
|
||||
|
|
@ -45348,7 +45377,7 @@ Issue type.
|
|||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="issuetypeepic"></a>`EPIC` {{< icon name="warning-solid" >}} | **Introduced** in GitLab 16.7. **Status**: Experiment. Epic issue type. Available only when feature epics is available and the feature flag `work_item_epics` is enabled. |
|
||||
| <a id="issuetypeepic"></a>`EPIC` {{< icon name="warning-solid" >}} | **Introduced** in GitLab 16.7. **Status**: Experiment. Epic issue type. Available only when feature epics is available. |
|
||||
| <a id="issuetypeincident"></a>`INCIDENT` | Incident issue type. |
|
||||
| <a id="issuetypeissue"></a>`ISSUE` | Issue issue type. |
|
||||
| <a id="issuetypekey_result"></a>`KEY_RESULT` {{< icon name="warning-solid" >}} | **Introduced** in GitLab 15.7. **Status**: Experiment. Key Result issue type. Available only when feature flag `okrs_mvc` is enabled. |
|
||||
|
|
@ -48779,6 +48808,12 @@ A `WorkItemsRelatedWorkItemLinkID` is a global ID. It is encoded as a string.
|
|||
|
||||
An example `WorkItemsRelatedWorkItemLinkID` is: `"gid://gitlab/WorkItems::RelatedWorkItemLink/1"`.
|
||||
|
||||
### `WorkItemsStatusesLifecycleID`
|
||||
|
||||
A `WorkItemsStatusesLifecycleID` is a global ID. It is encoded as a string.
|
||||
|
||||
An example `WorkItemsStatusesLifecycleID` is: `"gid://gitlab/WorkItems::Statuses::Lifecycle/1"`.
|
||||
|
||||
### `WorkItemsStatusesStatusID`
|
||||
|
||||
A `WorkItemsStatusesStatusID` is a global ID. It is encoded as a string.
|
||||
|
|
@ -51418,6 +51453,18 @@ Attributes for value stream setting.
|
|||
| <a id="workitemresolvediscussionsinputdiscussionid"></a>`discussionId` | [`String`](#string) | ID of a discussion to resolve. |
|
||||
| <a id="workitemresolvediscussionsinputnoteableid"></a>`noteableId` | [`NoteableID!`](#noteableid) | Global ID of the noteable where discussions will be resolved when the work item is created. Only `MergeRequestID` is supported at the moment. |
|
||||
|
||||
### `WorkItemStatusInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="workitemstatusinputcategory"></a>`category` | [`WorkItemStatusCategoryEnum`](#workitemstatuscategoryenum) | Category of the status. |
|
||||
| <a id="workitemstatusinputcolor"></a>`color` | [`String`](#string) | Color of the status. |
|
||||
| <a id="workitemstatusinputdescription"></a>`description` | [`String`](#string) | Description of the status. |
|
||||
| <a id="workitemstatusinputid"></a>`id` | [`GlobalID`](#globalid) | ID of the status. If not provided, a new status will be created. |
|
||||
| <a id="workitemstatusinputname"></a>`name` | [`String`](#string) | Name of the status. |
|
||||
|
||||
### `WorkItemWidgetAssigneesInput`
|
||||
|
||||
#### Arguments
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ title: Vulnerability export API
|
|||
|
||||
Every API call to vulnerability exports must be [authenticated](rest/authentication.md).
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/524058) PDF exports in GitLab 18.1 [with a flag](../administration/feature_flags.md) named `vulnerabilities_pdf_export`. Disabled by default.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
## Create a project-level vulnerability export
|
||||
|
||||
Creates a new vulnerability export for a project.
|
||||
|
|
@ -31,10 +37,19 @@ Vulnerability exports can be only accessed by the export's author.
|
|||
POST /security/projects/:id/vulnerability_exports
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|--------------|-------------------|----------|--------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path](rest/_index.md#namespaced-paths) of the project that the authenticated user is a member of |
|
||||
| `send_email` | boolean | no | When set to `true`, sends an email notification to the user who requested the export when the export completes. |
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------------|-------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path](rest/_index.md#namespaced-paths) of the project that the authenticated user is a member of |
|
||||
| `send_email` | boolean | no | When set to `true`, sends an email notification to the user who requested the export when the export completes. |
|
||||
| `export_format` | string | no | Values: `csv`,`pdf`. Default is `csv`. A `pdf` report requires the `vulnerabilities_pdf_export` feature flag. |
|
||||
| `report_data` | object | no | A hash of report components mapped to frontend data assets to use in the export. For example: `{ project_vulnerabilities_history: '<svg>some report asset</svg>' }` |
|
||||
|
||||
{{< alert type="flag" >}}
|
||||
|
||||
The availability of PDF exports is controlled by a feature flag. For more information, see
|
||||
the history.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/security/projects/1/vulnerability_exports"
|
||||
|
|
@ -81,9 +96,18 @@ Vulnerability exports can be only accessed by the export's author.
|
|||
POST /security/groups/:id/vulnerability_exports
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------------- | ----------------- | ---------- | -----------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path](rest/_index.md#namespaced-paths) of the group which the authenticated user is a member of |
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------------|-------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path](rest/_index.md#namespaced-paths) of the group which the authenticated user is a member of |
|
||||
| `export_format` | string | no | Values: `csv`,`pdf`. Default is `csv`. A PDF report requires the `vulnerabilities_pdf_export` feature flag. |
|
||||
| `report_data` | object | no | A hash of report components mapped to frontend data assets to use in the export. For example: `{ project_vulnerabilities_history: '<svg>some report asset</svg>' }` |
|
||||
|
||||
{{< alert type="flag" >}}
|
||||
|
||||
The availability of PDF exports is controlled by a feature flag. For more information, see
|
||||
the history.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/security/groups/1/vulnerability_exports"
|
||||
|
|
|
|||
|
|
@ -31,11 +31,6 @@ module API
|
|||
additional_properties = params.fetch(:additional_properties, {}).symbolize_keys
|
||||
send_snowplow_event = !!params[:send_to_snowplow]
|
||||
|
||||
if Gitlab::Tracking::AiTracking::EVENTS_MIGRATED_TO_INSTRUMENTATION_LAYER.exclude?(event_name)
|
||||
Gitlab::Tracking::AiTracking.track_event(event_name,
|
||||
**additional_properties.merge(user: current_user, project_id: project_id, namespace_id: namespace_id))
|
||||
end
|
||||
|
||||
track_event(
|
||||
event_name,
|
||||
send_snowplow_event: send_snowplow_event,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@
|
|||
module Gitlab
|
||||
module Tracking
|
||||
class AiTracking
|
||||
# events getting taken care of by instrumentation layer
|
||||
EVENTS_MIGRATED_TO_INSTRUMENTATION_LAYER = %w[request_duo_chat_response code_suggestion_shown_in_ide].freeze
|
||||
|
||||
def self.track_event(*args, **kwargs)
|
||||
new.track_event(*args, **kwargs)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28316,6 +28316,9 @@ msgstr ""
|
|||
msgid "GitLab Premium"
|
||||
msgstr ""
|
||||
|
||||
msgid "GitLab Query Language (GLQL) view"
|
||||
msgstr ""
|
||||
|
||||
msgid "GitLab Security"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -33941,6 +33944,9 @@ msgstr ""
|
|||
msgid "Issues are being rebalanced at the moment, so manual reordering is disabled."
|
||||
msgstr ""
|
||||
|
||||
msgid "Issues assigned to current user"
|
||||
msgstr ""
|
||||
|
||||
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -37212,6 +37218,12 @@ msgstr ""
|
|||
msgid "MarkdownEditor|Find and replace"
|
||||
msgstr ""
|
||||
|
||||
msgid "MarkdownEditor|Find next"
|
||||
msgstr ""
|
||||
|
||||
msgid "MarkdownEditor|Find previous"
|
||||
msgstr ""
|
||||
|
||||
msgid "MarkdownEditor|Indent line (%{modifierKey}])"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -50508,6 +50520,9 @@ msgstr ""
|
|||
msgid "RapidDiffs|Failed to load changes, please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "RapidDiffs|File browser"
|
||||
msgstr ""
|
||||
|
||||
msgid "RapidDiffs|File moved from %{old} to %{new}"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
"@gitlab/ui": "113.7.0",
|
||||
"@gitlab/vue-router-vue3": "npm:vue-router@4.5.1",
|
||||
"@gitlab/vuex-vue3": "npm:vuex@4.1.0",
|
||||
"@gitlab/web-ide": "^0.0.1-dev-20250528064209",
|
||||
"@gitlab/web-ide": "^0.0.1-dev-20250611141528",
|
||||
"@gleam-lang/highlight.js-gleam": "^1.5.0",
|
||||
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
|
||||
"@rails/actioncable": "7.1.501",
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ describe('Blob Content component', () => {
|
|||
expect(wrapper.findComponent(SimpleViewer).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('passes isSnippet prop to the viewer', () => {
|
||||
createComponent({ isSnippet: true }, RichViewerMock);
|
||||
expect(wrapper.findComponent(RichViewer).props('isSnippet')).toBe(true);
|
||||
});
|
||||
|
||||
it.each`
|
||||
type | mock | viewer
|
||||
${'simple'} | ${SimpleViewerMock} | ${SimpleViewer}
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ describe('Board List Header Component', () => {
|
|||
ListType.milestone,
|
||||
ListType.iteration,
|
||||
ListType.assignee,
|
||||
ListType.status,
|
||||
];
|
||||
|
||||
it.each(hasNoAddButton)('does not render dropdown when List Type is `%s`', (listType) => {
|
||||
|
|
|
|||
|
|
@ -600,6 +600,32 @@ export const mockLabelList = {
|
|||
__typename: 'BoardList',
|
||||
};
|
||||
|
||||
export const mockStatusList = {
|
||||
id: 'gid://gitlab/List/5',
|
||||
title: 'In Progress',
|
||||
position: 0,
|
||||
listType: 'status',
|
||||
collapsed: false,
|
||||
label: null,
|
||||
assignee: null,
|
||||
milestone: null,
|
||||
iteration: null,
|
||||
loading: false,
|
||||
issuesCount: 0,
|
||||
maxIssueCount: 0,
|
||||
maxIssueWeight: 0,
|
||||
status: {
|
||||
id: 'gid://gitlab/WorkItems::Statuses::SystemDefined::Status/2',
|
||||
name: 'In progress',
|
||||
iconName: 'status-running',
|
||||
color: '#1f75cb',
|
||||
position: 0,
|
||||
__typename: 'WorkItemStatus',
|
||||
},
|
||||
limitMetric: 'issue_count',
|
||||
__typename: 'BoardList',
|
||||
};
|
||||
|
||||
export const mockMilestoneList = {
|
||||
id: 'gid://gitlab/List/3',
|
||||
title: 'To Do',
|
||||
|
|
@ -613,6 +639,7 @@ export const mockMilestoneList = {
|
|||
title: 'Backlog',
|
||||
},
|
||||
loading: false,
|
||||
status: null,
|
||||
issuesCount: 0,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -267,6 +267,33 @@ describe('PipelineInputsForm', () => {
|
|||
expect(findInputsTable().exists()).toBe(false);
|
||||
expect(findEmptySelectionState().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('selects all inputs on select all button click', async () => {
|
||||
findInputsSelector().vm.$emit('select-all');
|
||||
await nextTick();
|
||||
|
||||
const updatedSelection = [
|
||||
{ ...expectedInputs[0], isSelected: true },
|
||||
{ ...expectedInputs[1], isSelected: true },
|
||||
{ ...expectedInputs[2], isSelected: true },
|
||||
];
|
||||
expect(findInputsTable().props('inputs')).toEqual(updatedSelection);
|
||||
});
|
||||
|
||||
it('selects only filtered inputs when search is active', async () => {
|
||||
findInputsSelector().vm.$emit('search', 'api');
|
||||
await nextTick();
|
||||
|
||||
findInputsSelector().vm.$emit('select-all');
|
||||
await nextTick();
|
||||
|
||||
const updatedSelection = findInputsTable().props('inputs');
|
||||
const apiTokenInput = updatedSelection.find((i) => i.name === 'api_token');
|
||||
const otherInputs = updatedSelection.filter((i) => i.name !== 'api_token');
|
||||
|
||||
expect(apiTokenInput.isSelected).toBe(true);
|
||||
expect(otherInputs.every((i) => !i.isSelected)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlDisclosureDropdown } from '@gitlab/ui';
|
||||
import { GlDisclosureDropdown, GlBadge } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ToolbarMoreDropdown from '~/content_editor/components/toolbar_more_dropdown.vue';
|
||||
import Diagram from '~/content_editor/extensions/diagram';
|
||||
|
|
@ -38,18 +38,19 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
name | contentType | command | params
|
||||
${'Alert'} | ${'alert'} | ${'insertAlert'} | ${[]}
|
||||
${'Code block'} | ${'codeBlock'} | ${'setNode'} | ${['codeBlock']}
|
||||
${'Details block'} | ${'details'} | ${'toggleList'} | ${['details', 'detailsContent']}
|
||||
${'Bullet list'} | ${'bulletList'} | ${'toggleList'} | ${['bulletList', 'listItem']}
|
||||
${'Ordered list'} | ${'orderedList'} | ${'toggleList'} | ${['orderedList', 'listItem']}
|
||||
${'Task list'} | ${'taskList'} | ${'toggleList'} | ${['taskList', 'taskItem']}
|
||||
${'Mermaid diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'mermaid' }]}
|
||||
${'PlantUML diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'plantuml' }]}
|
||||
${'Table of contents'} | ${'tableOfContents'} | ${'insertTableOfContents'} | ${[]}
|
||||
${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]}
|
||||
${'Create or edit diagram'} | ${'drawioDiagram'} | ${'createOrEditDiagram'} | ${[]}
|
||||
name | contentType | command | params
|
||||
${'Alert'} | ${'alert'} | ${'insertAlert'} | ${[]}
|
||||
${'Code block'} | ${'codeBlock'} | ${'setNode'} | ${['codeBlock']}
|
||||
${'Details block'} | ${'details'} | ${'toggleList'} | ${['details', 'detailsContent']}
|
||||
${'Bullet list'} | ${'bulletList'} | ${'toggleList'} | ${['bulletList', 'listItem']}
|
||||
${'Ordered list'} | ${'orderedList'} | ${'toggleList'} | ${['orderedList', 'listItem']}
|
||||
${'Task list'} | ${'taskList'} | ${'toggleList'} | ${['taskList', 'taskItem']}
|
||||
${'Mermaid diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'mermaid' }]}
|
||||
${'PlantUML diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'plantuml' }]}
|
||||
${'Table of contents'} | ${'tableOfContents'} | ${'insertTableOfContents'} | ${[]}
|
||||
${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]}
|
||||
${'Create or edit diagram'} | ${'drawioDiagram'} | ${'createOrEditDiagram'} | ${[]}
|
||||
${'GitLab Query Language (GLQL) view Beta'} | ${'glqlView'} | ${'insertGLQLView'} | ${[]}
|
||||
`('when option $name is clicked', ({ name, command, contentType, params }) => {
|
||||
let commands;
|
||||
let btn;
|
||||
|
|
@ -90,4 +91,19 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a beta badge for the GLQL view option', () => {
|
||||
buildWrapper();
|
||||
|
||||
const btn = wrapper.findByRole('button', { name: 'GitLab Query Language (GLQL) view Beta' });
|
||||
const badge = wrapper.findComponent(GlBadge);
|
||||
|
||||
expect(btn.exists()).toBe(true);
|
||||
expect(badge.props()).toMatchObject({
|
||||
variant: 'info',
|
||||
target: '_blank',
|
||||
href: '/help/user/glql/_index',
|
||||
});
|
||||
expect(badge.text()).toBe('Beta');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -97,4 +97,21 @@ describe('content_editor/extensions/code_block_highlight', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when inserting a GLQL view', () => {
|
||||
beforeEach(() => {
|
||||
tiptapEditor.commands.insertGLQLView();
|
||||
});
|
||||
|
||||
it('inserts a GLQL view', () => {
|
||||
expect(tiptapEditor.getJSON()).toEqual(
|
||||
doc(
|
||||
codeBlock(
|
||||
{ language: 'glql' },
|
||||
'query: assignee = currentUser()\nfields: title, createdAt, milestone, assignee\ntitle: Issues assigned to current user',
|
||||
),
|
||||
).toJSON(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ describe('DiffAppControls', () => {
|
|||
diffsCount: DEFAULT_PROPS.diffsCount,
|
||||
addedLines: DEFAULT_PROPS.addedLines,
|
||||
removedLines: DEFAULT_PROPS.removedLines,
|
||||
hideOnNarrowScreen: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -34,4 +34,20 @@ describe('FileBrowser store', () => {
|
|||
expect(useFileBrowser().fileBrowserVisible).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('browser drawer visibility', () => {
|
||||
it('is hidden by default', () => {
|
||||
expect(useFileBrowser().fileBrowserDrawerVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('#setFileBrowserDrawerVisibility', () => {
|
||||
useFileBrowser().setFileBrowserDrawerVisibility(true);
|
||||
expect(useFileBrowser().fileBrowserDrawerVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('#toggleFileBrowserDrawerVisibility', () => {
|
||||
useFileBrowser().toggleFileBrowserDrawerVisibility();
|
||||
expect(useFileBrowser().fileBrowserDrawerVisible).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -459,14 +459,20 @@ describe('MergeRequestTabs', () => {
|
|||
describe('Rapid Diffs', () => {
|
||||
let createRapidDiffsApp;
|
||||
let init;
|
||||
let hide;
|
||||
let show;
|
||||
let reloadDiffs;
|
||||
|
||||
beforeEach(() => {
|
||||
setWindowLocation('https://example.com?rapid_diffs=true');
|
||||
reloadDiffs = jest.fn();
|
||||
init = jest.fn();
|
||||
hide = jest.fn();
|
||||
show = jest.fn();
|
||||
createRapidDiffsApp = jest.fn(() => ({
|
||||
init,
|
||||
hide,
|
||||
show,
|
||||
reloadDiffs,
|
||||
}));
|
||||
});
|
||||
|
|
@ -494,6 +500,27 @@ describe('MergeRequestTabs', () => {
|
|||
expect(init).toHaveBeenCalledTimes(1);
|
||||
expect(reloadDiffs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('hides Rapid Diffs', () => {
|
||||
testContext.class = new MergeRequestTabs({
|
||||
stubLocation,
|
||||
createRapidDiffsApp,
|
||||
});
|
||||
testContext.class.tabShown('diffs', 'not-a-vue-page');
|
||||
testContext.class.tabShown('new', 'not-a-vue-page');
|
||||
expect(hide).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows Rapid Diffs', () => {
|
||||
testContext.class = new MergeRequestTabs({
|
||||
stubLocation,
|
||||
createRapidDiffsApp,
|
||||
});
|
||||
testContext.class.tabShown('diffs', 'not-a-vue-page');
|
||||
testContext.class.tabShown('new', 'not-a-vue-page');
|
||||
testContext.class.tabShown('diffs', 'not-a-vue-page');
|
||||
expect(show).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -478,6 +478,22 @@ describe('List Page', () => {
|
|||
DELETE_IMAGE_ERROR_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path),
|
||||
);
|
||||
});
|
||||
|
||||
it('shows an alert with multiple errors', async () => {
|
||||
mountComponent();
|
||||
|
||||
await selectImageForDeletion();
|
||||
|
||||
const errors = [{ message: 'Error 1' }, { message: 'Error 2' }, { message: 'Error 3' }];
|
||||
findDeleteImage().vm.$emit('error', errors);
|
||||
await nextTick();
|
||||
|
||||
expect(findDeleteAlert().exists()).toBe(true);
|
||||
expect(findDeleteAlert().text()).toMatch(/^Something went wrong while scheduling/);
|
||||
expect(findDeleteAlert().text()).toContain('Error 1');
|
||||
expect(findDeleteAlert().text()).toContain('Error 2');
|
||||
expect(findDeleteAlert().text()).toContain('Error 3');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useViewport } from '~/pinia/global_stores/viewport';
|
||||
import { isNarrowScreenMediaQuery } from '~/lib/utils/css_utils';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
jest.mock('~/lib/utils/css_utils');
|
||||
|
||||
describe('Viewport store', () => {
|
||||
beforeEach(() => {
|
||||
createTestingPinia({ stubActions: false });
|
||||
});
|
||||
|
||||
describe('isNarrowScreen', () => {
|
||||
let handler;
|
||||
|
||||
const setNarrowScreen = (isNarrow) => {
|
||||
isNarrowScreenMediaQuery.mockReturnValue({
|
||||
matches: isNarrow,
|
||||
addEventListener: jest.fn((_, fn) => {
|
||||
handler = fn;
|
||||
}),
|
||||
});
|
||||
};
|
||||
const triggerChange = (isNarrow) => {
|
||||
handler({ matches: isNarrow });
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
isNarrowScreenMediaQuery.mockReset();
|
||||
});
|
||||
|
||||
it('returns true if screen is narrow', () => {
|
||||
setNarrowScreen(true);
|
||||
expect(useViewport().isNarrowScreen).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if screen is not narrow', () => {
|
||||
setNarrowScreen(false);
|
||||
expect(useViewport().isNarrowScreen).toBe(false);
|
||||
});
|
||||
|
||||
it('updates value if screen changes', async () => {
|
||||
setNarrowScreen(true);
|
||||
useViewport();
|
||||
triggerChange(false);
|
||||
await waitForPromises();
|
||||
expect(useViewport().isNarrowScreen).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -13,6 +13,7 @@ import { fixWebComponentsStreamingOnSafari } from '~/rapid_diffs/app/safari_fix'
|
|||
import { DIFF_FILE_MOUNTED } from '~/rapid_diffs/dom_events';
|
||||
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
|
||||
import { disableBrokenContentVisibility } from '~/rapid_diffs/app/content_visibility_fix';
|
||||
import { useApp } from '~/rapid_diffs/stores/app';
|
||||
|
||||
jest.mock('~/lib/graphql');
|
||||
jest.mock('~/awards_handler');
|
||||
|
|
@ -149,6 +150,19 @@ describe('Rapid Diffs App', () => {
|
|||
expect(app.appData.shouldSortMetadataFiles).toBe(false);
|
||||
});
|
||||
|
||||
it('hides the app', () => {
|
||||
createApp();
|
||||
app.hide();
|
||||
expect(useApp().appVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the app', () => {
|
||||
createApp();
|
||||
app.hide();
|
||||
app.show();
|
||||
expect(useApp().appVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('delegates clicks', () => {
|
||||
const onClick = jest.fn();
|
||||
createApp();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { GlDrawer } from '@gitlab/ui';
|
||||
import FileBrowserDrawer from '~/rapid_diffs/app/file_browser_drawer.vue';
|
||||
import DiffsFileTree from '~/diffs/components/diffs_file_tree.vue';
|
||||
import { useDiffsList } from '~/rapid_diffs/stores/diffs_list';
|
||||
import { useFileBrowser } from '~/diffs/stores/file_browser';
|
||||
import { useDiffsView } from '~/rapid_diffs/stores/diffs_view';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
|
||||
Vue.use(PiniaVuePlugin);
|
||||
|
||||
describe('FileBrowserDrawer', () => {
|
||||
let wrapper;
|
||||
let pinia;
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMount(FileBrowserDrawer, {
|
||||
pinia,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
pinia = createTestingPinia();
|
||||
useDiffsList();
|
||||
useDiffsView();
|
||||
useFileBrowser();
|
||||
});
|
||||
|
||||
it('passes down props', () => {
|
||||
const loadedFiles = { foo: 1 };
|
||||
const totalFilesCount = 20;
|
||||
useDiffsList().loadedFiles = loadedFiles;
|
||||
useDiffsView().diffsStats = { diffsCount: totalFilesCount };
|
||||
createComponent();
|
||||
const tree = wrapper.findComponent(DiffsFileTree);
|
||||
expect(tree.props('loadedFiles')).toStrictEqual(loadedFiles);
|
||||
expect(tree.props('totalFilesCount')).toStrictEqual(totalFilesCount);
|
||||
});
|
||||
|
||||
it('is hidden by default', () => {
|
||||
createComponent();
|
||||
expect(wrapper.findComponent(GlDrawer).props('open')).toBe(false);
|
||||
});
|
||||
|
||||
it('shows file browser', async () => {
|
||||
createComponent();
|
||||
useFileBrowser().fileBrowserDrawerVisible = true;
|
||||
await nextTick();
|
||||
expect(wrapper.findComponent(GlDrawer).props('open')).toBe(true);
|
||||
});
|
||||
|
||||
it('handles click', async () => {
|
||||
const file = { fileHash: 'foo' };
|
||||
createComponent();
|
||||
await wrapper.findComponent(DiffsFileTree).vm.$emit('clickFile', file);
|
||||
expect(wrapper.emitted('clickFile')).toStrictEqual([[file]]);
|
||||
expect(useFileBrowser().setFileBrowserDrawerVisibility).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('updates state on destroy', () => {
|
||||
createComponent();
|
||||
wrapper.destroy();
|
||||
expect(useFileBrowser().setFileBrowserDrawerVisibility).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('handles toggleFolder', async () => {
|
||||
const path = 'foo';
|
||||
createComponent();
|
||||
await wrapper.findComponent(DiffsFileTree).vm.$emit('toggleFolder', path);
|
||||
expect(useLegacyDiffs().toggleTreeOpen).toHaveBeenCalledWith(path);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import Vue from 'vue';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import FileBrowserDrawerToggle from '~/rapid_diffs/app/file_browser_drawer_toggle.vue';
|
||||
import { useFileBrowser } from '~/diffs/stores/file_browser';
|
||||
|
||||
Vue.use(PiniaVuePlugin);
|
||||
|
||||
describe('FileBrowserDrawerToggle', () => {
|
||||
let wrapper;
|
||||
let pinia;
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMount(FileBrowserDrawerToggle, {
|
||||
pinia,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
pinia = createTestingPinia();
|
||||
useFileBrowser();
|
||||
});
|
||||
|
||||
it('toggles file browser drawer', () => {
|
||||
createComponent();
|
||||
wrapper.findComponent(GlButton).vm.$emit('click');
|
||||
expect(useFileBrowser().toggleFileBrowserDrawerVisibility).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { nextTick } from 'vue';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
|
||||
import { initFileBrowser } from '~/rapid_diffs/app/init_file_browser';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { DiffFile } from '~/rapid_diffs/diff_file';
|
||||
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import store from '~/mr_notes/stores';
|
||||
import { SET_TREE_DATA } from '~/diffs/store/mutation_types';
|
||||
import { useViewport } from '~/pinia/global_stores/viewport';
|
||||
import { pinia } from '~/pinia/instance';
|
||||
import { useApp } from '~/rapid_diffs/stores/app';
|
||||
|
||||
jest.mock('~/rapid_diffs/app/file_browser.vue', () => ({
|
||||
props: jest.requireActual('~/rapid_diffs/app/file_browser.vue').default.props,
|
||||
|
|
@ -26,6 +27,23 @@ jest.mock('~/rapid_diffs/app/file_browser.vue', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('~/rapid_diffs/app/file_browser_drawer.vue', () => ({
|
||||
props: jest.requireActual('~/rapid_diffs/app/file_browser_drawer.vue').default.props,
|
||||
render(h) {
|
||||
return h('div', {
|
||||
attrs: {
|
||||
'data-file-browser-drawer-component': true,
|
||||
'data-group-blobs-list-items': JSON.stringify(this.groupBlobsListItems),
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.$emit('clickFile', { fileHash: 'first' });
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('~/diffs/components/file_browser_toggle.vue', () => ({
|
||||
render(h) {
|
||||
return h('div', {
|
||||
|
|
@ -36,14 +54,28 @@ jest.mock('~/diffs/components/file_browser_toggle.vue', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('~/rapid_diffs/app/file_browser_drawer_toggle.vue', () => ({
|
||||
render(h) {
|
||||
return h('div', {
|
||||
attrs: {
|
||||
'data-file-browser-drawer-toggle-component': true,
|
||||
},
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Init file browser', () => {
|
||||
let mockAxios;
|
||||
let commit;
|
||||
let appData;
|
||||
|
||||
const getFileBrowserTarget = () => document.querySelector('[data-file-browser]');
|
||||
const getFileBrowserToggleTarget = () => document.querySelector('[data-file-browser-toggle]');
|
||||
const getFileBrowser = () => document.querySelector('[data-file-browser-component]');
|
||||
const getFileBrowserDrawer = () => document.querySelector('[data-file-browser-drawer-component]');
|
||||
const getFileBrowserToggle = () => document.querySelector('[data-file-browser-toggle-component]');
|
||||
const getFileBrowserDrawerToggle = () =>
|
||||
document.querySelector('[data-file-browser-drawer-toggle-component]');
|
||||
|
||||
const createDiffFiles = () => [
|
||||
{
|
||||
conflict_type: null,
|
||||
|
|
@ -90,20 +122,23 @@ describe('Init file browser', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(pinia);
|
||||
initAppData();
|
||||
window.mrTabs = { eventHub: createEventHub() };
|
||||
useViewport().reset();
|
||||
useApp().$reset();
|
||||
|
||||
mockAxios = new MockAdapter(axios);
|
||||
mockAxios
|
||||
.onGet(appData.diffFilesEndpoint)
|
||||
.reply(HTTP_STATUS_OK, { diff_files: createDiffFiles() });
|
||||
commit = jest.spyOn(store, 'commit');
|
||||
setHTMLFixture(
|
||||
`
|
||||
<div data-file-browser-toggle></div>
|
||||
<div data-file-browser data-metadata-endpoint="/metadata"></div>
|
||||
<diff-file data-file-data="{}" id="first"><div></div></diff-file>
|
||||
`,
|
||||
);
|
||||
|
||||
setHTMLFixture(`
|
||||
<div id="js-page-breadcrumbs-extra"></div>
|
||||
<div data-file-browser-toggle></div>
|
||||
<div data-file-browser data-metadata-endpoint="/metadata"></div>
|
||||
<diff-file data-file-data="{}" id="first"><div></div></diff-file>
|
||||
`);
|
||||
|
||||
DiffFile.getAll().forEach((file) =>
|
||||
file.mount({ adapterConfig: {}, appData: {}, unobserve: jest.fn() }),
|
||||
);
|
||||
|
|
@ -117,42 +152,62 @@ describe('Init file browser', () => {
|
|||
resetHTMLFixture();
|
||||
});
|
||||
|
||||
it('mounts the component', async () => {
|
||||
await init();
|
||||
expect(getFileBrowser()).not.toBe(null);
|
||||
});
|
||||
describe.each`
|
||||
isNarrowScreen | getBrowserElement | getBrowserToggleElement
|
||||
${false} | ${getFileBrowser} | ${getFileBrowserToggle}
|
||||
${true} | ${getFileBrowserDrawer} | ${getFileBrowserDrawerToggle}
|
||||
`(
|
||||
'when narrow screen is $isNarrowScreen',
|
||||
({ isNarrowScreen, getBrowserElement, getBrowserToggleElement }) => {
|
||||
beforeEach(() => {
|
||||
useViewport().updateIsNarrow(isNarrowScreen);
|
||||
});
|
||||
|
||||
it('mounts the components', async () => {
|
||||
await init();
|
||||
|
||||
expect(getBrowserElement()).not.toBe(null);
|
||||
expect(getBrowserToggleElement()).not.toBe(null);
|
||||
});
|
||||
|
||||
it('handles file clicks', async () => {
|
||||
const selectFile = jest.fn();
|
||||
const spy = jest.spyOn(DiffFile, 'findByFileHash').mockReturnValue({ selectFile });
|
||||
|
||||
await init();
|
||||
|
||||
const fileBrowser = getBrowserElement();
|
||||
fileBrowser.click();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('first');
|
||||
expect(selectFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes sorting configuration to components', async () => {
|
||||
await init();
|
||||
expect(document.querySelector('[data-group-blobs-list-items="true"]')).not.toBe(null);
|
||||
});
|
||||
|
||||
it('disables sorting when configured', async () => {
|
||||
initAppData({ shouldSortMetadataFiles: false });
|
||||
await init();
|
||||
expect(document.querySelector('[data-group-blobs-list-items="false"]')).not.toBe(null);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('loads diff files data', async () => {
|
||||
await init();
|
||||
expect(commit).toHaveBeenCalledWith(
|
||||
`diffs/${SET_TREE_DATA}`,
|
||||
expect.objectContaining({
|
||||
tree: expect.any(Array),
|
||||
treeEntries: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockAxios.history.get).toHaveLength(1);
|
||||
expect(mockAxios.history.get[0].url).toBe('/diff-files-metadata');
|
||||
});
|
||||
|
||||
it('handles file clicks', async () => {
|
||||
const selectFile = jest.fn();
|
||||
const spy = jest.spyOn(DiffFile, 'findByFileHash').mockReturnValue({ selectFile });
|
||||
init();
|
||||
await waitForPromises();
|
||||
getFileBrowser().click();
|
||||
expect(spy).toHaveBeenCalledWith('first');
|
||||
expect(selectFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows file browser toggle', async () => {
|
||||
init();
|
||||
await waitForPromises();
|
||||
expect(document.querySelector('[data-file-browser-toggle-component]')).not.toBe(null);
|
||||
});
|
||||
|
||||
it('disables sorting', async () => {
|
||||
initAppData({ shouldSortMetadataFiles: false });
|
||||
init();
|
||||
await waitForPromises();
|
||||
expect(document.querySelector('[data-group-blobs-list-items="false"]')).not.toBe(null);
|
||||
it('hides drawer toggle when app is hidden', async () => {
|
||||
useViewport().updateIsNarrow(true);
|
||||
await init();
|
||||
useApp().appVisible = false;
|
||||
await nextTick();
|
||||
expect(getFileBrowserDrawerToggle()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ jest.mock('~/diffs/components/diff_app_controls.vue', () => ({
|
|||
'data-removed-lines': JSON.stringify(this.removedLines),
|
||||
'data-diffs-count': JSON.stringify(this.diffsCount),
|
||||
'data-file-by-file-supported': JSON.stringify(this.fileByFileSupported),
|
||||
'data-hide-on-narrow-screen': JSON.stringify(this.hideOnNarrowScreen),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
@ -95,6 +96,7 @@ describe('View settings', () => {
|
|||
expect(getProp('removedLines')).toBe(2);
|
||||
expect(getProp('diffsCount')).toBe(3);
|
||||
expect(getProp('fileByFileSupported')).toBe(false);
|
||||
expect(getProp('hideOnNarrowScreen')).toBe(false);
|
||||
});
|
||||
|
||||
it('triggers collapse all files', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useApp } from '~/rapid_diffs/stores/app';
|
||||
|
||||
describe('rapidDiffsApp store', () => {
|
||||
beforeEach(() => {
|
||||
createTestingPinia({ stubActions: false });
|
||||
});
|
||||
|
||||
it('is visible by default', () => {
|
||||
expect(useApp().appVisible).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -126,6 +126,7 @@ describe('Blob Embeddable', () => {
|
|||
});
|
||||
await waitForPromises();
|
||||
expect(findRichViewer().exists()).toBe(true);
|
||||
expect(findRichViewer().props('isSnippet')).toBe(true);
|
||||
});
|
||||
|
||||
it('correctly switches viewer type', async () => {
|
||||
|
|
|
|||
|
|
@ -17,12 +17,19 @@ describe('Blob Rich Viewer component', () => {
|
|||
const dummyContent = '<h1 id="markdown">Foo Bar</h1>';
|
||||
const defaultType = 'markdown';
|
||||
|
||||
function createComponent(type = defaultType, richViewer, content = dummyContent) {
|
||||
// eslint-disable-next-line max-params
|
||||
function createComponent(
|
||||
type = defaultType,
|
||||
richViewer,
|
||||
content = dummyContent,
|
||||
isSnippet = false,
|
||||
) {
|
||||
wrapper = shallowMount(RichViewer, {
|
||||
propsData: {
|
||||
richViewer,
|
||||
content,
|
||||
type,
|
||||
isSnippet,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -66,6 +73,7 @@ describe('Blob Rich Viewer component', () => {
|
|||
});
|
||||
|
||||
it('sanitizes the content', () => {
|
||||
createComponent(MARKUP_FILE_TYPE, null, content, true);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(wrapper.html()).toContain('<img src="x">');
|
||||
|
|
@ -83,6 +91,7 @@ describe('Blob Rich Viewer component', () => {
|
|||
});
|
||||
|
||||
it('sanitizes the content', () => {
|
||||
createComponent(MARKUP_FILE_TYPE, null, content, true);
|
||||
expect(wrapper.html()).toContain('<img src="x">');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ describe('Markdown field header component', () => {
|
|||
const field = document.createElement('div');
|
||||
const root = document.createElement('div');
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = 'lorem ipsum dolor sit amet <img src="prompt">';
|
||||
textarea.value = 'lorem ipsum dolor sit amet lorem <img src="prompt">';
|
||||
field.classList = 'js-vue-markdown-field';
|
||||
form.classList = 'md-area';
|
||||
form.appendChild(textarea);
|
||||
|
|
@ -378,6 +378,8 @@ describe('Markdown field header component', () => {
|
|||
const findFindInput = () => wrapper.findByTestId('find-btn');
|
||||
const findCloneDiv = () => formWrapper.findByTestId('find-and-replace-clone');
|
||||
const findFindAndReplaceBar = () => wrapper.findByTestId('find-and-replace');
|
||||
const findNextButton = () => wrapper.findByTestId('find-next');
|
||||
const findPrevButton = () => wrapper.findByTestId('find-prev');
|
||||
|
||||
const showFindAndReplace = async () => {
|
||||
$(document).triggerHandler('markdown-editor:find-and-replace:show', [$('form')]);
|
||||
|
|
@ -464,7 +466,7 @@ describe('Markdown field header component', () => {
|
|||
await nextTick();
|
||||
|
||||
expect(findCloneDiv().element.innerHTML).toBe(
|
||||
'lorem ipsum dolor sit amet <img src="<span class="js-highlight" style="background-color: orange; display: inline-block;">prompt</span>">',
|
||||
'lorem ipsum dolor sit amet lorem <img src="<span class="js-highlight js-highlight-active" style="background-color: rgb(230, 228, 242); display: inline-block;">prompt</span>">',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -481,7 +483,57 @@ describe('Markdown field header component', () => {
|
|||
await findFindInput().vm.$emit('keyup', { target: { value: 'lorem' } });
|
||||
await nextTick();
|
||||
|
||||
expect(findFindAndReplaceBar().text()).toBe('1 of 1');
|
||||
expect(findFindAndReplaceBar().text()).toBe('1 of 2');
|
||||
});
|
||||
|
||||
it('highlights first item when there is a match', async () => {
|
||||
await showFindAndReplace();
|
||||
|
||||
// Text that matches
|
||||
await findFindInput().vm.$emit('keyup', { target: { value: 'lorem' } });
|
||||
await nextTick();
|
||||
|
||||
expect(findCloneDiv().element.querySelectorAll('.js-highlight-active').length).toBe(1);
|
||||
});
|
||||
|
||||
it('allows navigating between matches through next and prev buttons', async () => {
|
||||
await showFindAndReplace();
|
||||
|
||||
// Text that matches
|
||||
await findFindInput().vm.$emit('keyup', { target: { value: 'lorem' } });
|
||||
await nextTick();
|
||||
|
||||
const matches = findCloneDiv().element.querySelectorAll('.js-highlight');
|
||||
|
||||
expect(matches.length).toBe(2);
|
||||
expect(Array.from(matches[0].classList)).toEqual(['js-highlight', 'js-highlight-active']);
|
||||
expect(Array.from(matches[1].classList)).toEqual(['js-highlight']);
|
||||
|
||||
findNextButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
expect(Array.from(matches[0].classList)).toEqual(['js-highlight']);
|
||||
expect(Array.from(matches[1].classList)).toEqual(['js-highlight', 'js-highlight-active']);
|
||||
|
||||
findPrevButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
expect(Array.from(matches[0].classList)).toEqual(['js-highlight', 'js-highlight-active']);
|
||||
expect(Array.from(matches[1].classList)).toEqual(['js-highlight']);
|
||||
|
||||
// Click again to navigate to last item
|
||||
findPrevButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
expect(Array.from(matches[0].classList)).toEqual(['js-highlight']);
|
||||
expect(Array.from(matches[1].classList)).toEqual(['js-highlight', 'js-highlight-active']);
|
||||
|
||||
// Now that we're at last match, clicking next will bring us back to index 0
|
||||
findNextButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
expect(Array.from(matches[0].classList)).toEqual(['js-highlight', 'js-highlight-active']);
|
||||
expect(Array.from(matches[1].classList)).toEqual(['js-highlight']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -219,29 +219,46 @@ RSpec.describe 'new tables missing sharding_key', feature_category: :organizatio
|
|||
end
|
||||
end
|
||||
|
||||
it 'ensures all organization_id columns are not nullable, have no default, and have a foreign key',
|
||||
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/527615' do
|
||||
it 'ensures all organization_id columns are not nullable, have no default, and have a foreign key' do
|
||||
loose_foreign_keys = Gitlab::Database::LooseForeignKeys.definitions.group_by(&:from_table)
|
||||
|
||||
sql = <<~SQL
|
||||
SELECT c.table_name,
|
||||
CASE WHEN c.column_default IS NOT NULL THEN 'has default' ELSE NULL END,
|
||||
CASE WHEN c.is_nullable::boolean THEN 'nullable / not null constraint missing' ELSE NULL END,
|
||||
CASE WHEN fk.name IS NULL THEN 'no foreign key' ELSE
|
||||
CASE WHEN fk.is_valid THEN NULL ELSE 'foreign key exist but it is not validated' END
|
||||
END
|
||||
FROM information_schema.columns c
|
||||
LEFT JOIN postgres_foreign_keys fk
|
||||
ON fk.constrained_table_name = c.table_name AND fk.constrained_columns = '{organization_id}' and fk.referenced_columns = '{id}'
|
||||
WHERE c.column_name = 'organization_id'
|
||||
AND (fk.referenced_table_name = 'organizations' OR fk.referenced_table_name IS NULL)
|
||||
AND (c.column_default IS NOT NULL OR c.is_nullable::boolean OR fk.name IS NULL OR NOT fk.is_valid)
|
||||
AND (c.table_schema = 'public')
|
||||
ORDER BY c.table_name;
|
||||
# Step 1: Get all tables with organization_id columns
|
||||
tables_sql = <<~SQL
|
||||
SELECT table_name
|
||||
FROM information_schema.columns
|
||||
WHERE column_name = 'organization_id'
|
||||
AND table_schema = 'public'
|
||||
ORDER BY table_name;
|
||||
SQL
|
||||
|
||||
# To add a table to this list, create an issue under https://gitlab.com/groups/gitlab-org/-/epics/11670.
|
||||
# Use https://gitlab.com/gitlab-org/gitlab/-/issues/476206 as an example.
|
||||
table_names = ApplicationRecord.connection.select_values(tables_sql)
|
||||
|
||||
# Step 2: Check each table individually to avoid complex joins
|
||||
organization_id_columns = []
|
||||
|
||||
# Process in batches of 50 to avoid statement timeout issues with large queries
|
||||
table_names.each_slice(50) do |table_batch|
|
||||
batch_conditions = table_batch.map do |table|
|
||||
table_name = ApplicationRecord.connection.quote(table)
|
||||
"c.table_name = #{table_name}"
|
||||
end.join(' OR ')
|
||||
|
||||
batch_sql = <<~SQL
|
||||
SELECT c.table_name,
|
||||
CASE WHEN c.column_default IS NOT NULL THEN 'has default' ELSE NULL END,
|
||||
CASE WHEN c.is_nullable::boolean THEN 'nullable / not null constraint missing' ELSE NULL END
|
||||
FROM information_schema.columns c
|
||||
WHERE c.column_name = 'organization_id'
|
||||
AND c.table_schema = 'public'
|
||||
AND (#{batch_conditions})
|
||||
ORDER BY c.table_name;
|
||||
SQL
|
||||
|
||||
batch_results = ApplicationRecord.connection.select_rows(batch_sql)
|
||||
organization_id_columns.concat(batch_results)
|
||||
end
|
||||
|
||||
# Step 3: Check foreign keys using Rails schema introspection
|
||||
work_in_progress = {
|
||||
"snippet_user_mentions" => "https://gitlab.com/gitlab-org/gitlab/-/issues/517825",
|
||||
"bulk_import_failures" => "https://gitlab.com/gitlab-org/gitlab/-/issues/517824",
|
||||
|
|
@ -297,22 +314,38 @@ RSpec.describe 'new tables missing sharding_key', feature_category: :organizatio
|
|||
"ci_runners" => "https://gitlab.com/gitlab-org/gitlab/-/issues/525293",
|
||||
"group_type_ci_runners" => "https://gitlab.com/gitlab-org/gitlab/-/issues/525293",
|
||||
"instance_type_ci_runner_machines" => "https://gitlab.com/gitlab-org/gitlab/-/issues/525293",
|
||||
"project_type_ci_runners" => "https://gitlab.com/gitlab-org/gitlab/-/issues/525293"
|
||||
"project_type_ci_runners" => "https://gitlab.com/gitlab-org/gitlab/-/issues/525293",
|
||||
"ci_runner_taggings_group_type" => "https://gitlab.com/gitlab-org/gitlab/-/issues/549027",
|
||||
"ci_runner_taggings_project_type" => "https://gitlab.com/gitlab-org/gitlab/-/issues/549028",
|
||||
"customer_relations_contacts" => "https://gitlab.com/gitlab-org/gitlab/-/issues/549029",
|
||||
"issue_tracker_data" => "https://gitlab.com/gitlab-org/gitlab/-/issues/549030",
|
||||
"jira_tracker_data" => "https://gitlab.com/gitlab-org/gitlab/-/issues/549032",
|
||||
"zentao_tracker_data" => "https://gitlab.com/gitlab-org/gitlab/-/issues/549043"
|
||||
}
|
||||
|
||||
has_lfk = ->(lfks) { lfks.any? { |k| k.options[:column] == 'organization_id' && k.to_table == 'organizations' } }
|
||||
|
||||
organization_id_columns = ApplicationRecord.connection.select_rows(sql)
|
||||
checks = organization_id_columns.reject { |column| work_in_progress[column[0]] }
|
||||
messages = checks.filter_map do |check|
|
||||
table_name, *violations = check
|
||||
columns_to_check = organization_id_columns.reject { |column| work_in_progress[column[0]] }
|
||||
messages = columns_to_check.filter_map do |column|
|
||||
table_name = column[0]
|
||||
violations = column[1..].compact
|
||||
|
||||
# Check foreign keys using Rails
|
||||
begin
|
||||
foreign_keys = ApplicationRecord.connection.foreign_keys(table_name)
|
||||
org_fk = foreign_keys.find { |fk| fk.column == 'organization_id' && fk.to_table == 'organizations' }
|
||||
|
||||
violations << 'no foreign key' unless org_fk || has_lfk.call(loose_foreign_keys.fetch(table_name, {}))
|
||||
rescue ActiveRecord::StatementInvalid
|
||||
# Table might not exist or be accessible
|
||||
violations << 'no foreign key'
|
||||
end
|
||||
|
||||
violations.delete_if do |v|
|
||||
(v == 'nullable / not null constraint missing' && has_null_check_constraint?(table_name, 'organization_id')) ||
|
||||
(v == 'no foreign key' && has_lfk.call(loose_foreign_keys.fetch(table_name, {})))
|
||||
end
|
||||
|
||||
" #{table_name} - #{violations.compact.join(', ')}" if violations.any?
|
||||
" #{table_name} - #{violations.join(', ')}" if violations.any?
|
||||
end
|
||||
|
||||
expect(messages).to be_empty, "Expected all organization_id columns to be not nullable, have no default, " \
|
||||
|
|
|
|||
|
|
@ -1477,10 +1477,10 @@
|
|||
dependencies:
|
||||
"@vue/devtools-api" "^6.0.0-beta.11"
|
||||
|
||||
"@gitlab/web-ide@^0.0.1-dev-20250528064209":
|
||||
version "0.0.1-dev-20250528064209"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20250528064209.tgz#1c7ba2d896df24ccddaba5a5e4788b3965b13ee5"
|
||||
integrity sha512-2mpTvcjPWvVBJmIW2RTciVhgcB6H3Rvw3ldgX0QcvyMKqcftjYzi2HXPK0UeonqwZyD4mr5U1U8917Hxjhvhug==
|
||||
"@gitlab/web-ide@^0.0.1-dev-20250611141528":
|
||||
version "0.0.1-dev-20250611141528"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20250611141528.tgz#2238a3e67ca1c3fed0c7b18a4c21ec236be75571"
|
||||
integrity sha512-B1CqznGSZrTltsGBuBCufoHqDf0AUOPfE/lv7ElYOBJJ0MPHMMfbk3rsF/5f2vKpJ5EvHMm6C1aegi5400Mkxw==
|
||||
|
||||
"@gleam-lang/highlight.js-gleam@^1.5.0":
|
||||
version "1.5.0"
|
||||
|
|
|
|||
Loading…
Reference in New Issue