Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c60d68bbac
commit
6e320396b2
|
@ -124,7 +124,6 @@ export default {
|
|||
return {
|
||||
treeWidth,
|
||||
diffFilesLength: 0,
|
||||
collapsedWarningDismissed: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -153,7 +152,7 @@ export default {
|
|||
'canMerge',
|
||||
'hasConflicts',
|
||||
]),
|
||||
...mapGetters('diffs', ['hasCollapsedFile', 'isParallelView', 'currentDiffIndex']),
|
||||
...mapGetters('diffs', ['whichCollapsedTypes', 'isParallelView', 'currentDiffIndex']),
|
||||
...mapGetters(['isNotesFetched', 'getNoteableData']),
|
||||
diffs() {
|
||||
if (!this.viewDiffsFileByFile) {
|
||||
|
@ -206,11 +205,7 @@ export default {
|
|||
visible = this.$options.alerts.ALERT_OVERFLOW_HIDDEN;
|
||||
} else if (this.isDiffHead && this.hasConflicts) {
|
||||
visible = this.$options.alerts.ALERT_MERGE_CONFLICT;
|
||||
} else if (
|
||||
this.hasCollapsedFile &&
|
||||
!this.collapsedWarningDismissed &&
|
||||
!this.viewDiffsFileByFile
|
||||
) {
|
||||
} else if (this.whichCollapsedTypes.automatic && !this.viewDiffsFileByFile) {
|
||||
visible = this.$options.alerts.ALERT_COLLAPSED_FILES;
|
||||
}
|
||||
|
||||
|
@ -429,9 +424,6 @@ export default {
|
|||
this.toggleShowTreeList(false);
|
||||
}
|
||||
},
|
||||
dismissCollapsedWarning() {
|
||||
this.collapsedWarningDismissed = true;
|
||||
},
|
||||
},
|
||||
minTreeWidth: MIN_TREE_WIDTH,
|
||||
maxTreeWidth: MAX_TREE_WIDTH,
|
||||
|
@ -464,7 +456,6 @@ export default {
|
|||
<collapsed-files-warning
|
||||
v-if="visibleWarning == $options.alerts.ALERT_COLLAPSED_FILES"
|
||||
:limited="isLimitedContainer"
|
||||
@dismiss="dismissCollapsedWarning"
|
||||
/>
|
||||
|
||||
<div
|
||||
|
|
|
@ -38,7 +38,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapGetters('diffs', [
|
||||
'hasCollapsedFile',
|
||||
'whichCollapsedTypes',
|
||||
'diffCompareDropdownTargetVersions',
|
||||
'diffCompareDropdownSourceVersions',
|
||||
]),
|
||||
|
@ -129,7 +129,7 @@ export default {
|
|||
{{ __('Show latest version') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
v-show="hasCollapsedFile"
|
||||
v-show="whichCollapsedTypes.any"
|
||||
variant="default"
|
||||
class="gl-mr-3"
|
||||
@click="expandAllFiles"
|
||||
|
|
|
@ -10,6 +10,8 @@ import eventHub from '../../notes/event_hub';
|
|||
import DiffFileHeader from './diff_file_header.vue';
|
||||
import DiffContent from './diff_content.vue';
|
||||
import { diffViewerErrors } from '~/ide/constants';
|
||||
import { collapsedType, isCollapsed } from '../diff_file';
|
||||
import { DIFF_FILE_AUTOMATIC_COLLAPSE, DIFF_FILE_MANUAL_COLLAPSE } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -44,7 +46,7 @@ export default {
|
|||
return {
|
||||
isLoadingCollapsedDiff: false,
|
||||
forkMessageVisible: false,
|
||||
isCollapsed: this.file.viewer.automaticallyCollapsed || false,
|
||||
isCollapsed: isCollapsed(this.file),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -71,7 +73,7 @@ export default {
|
|||
return this.file.viewer.error === diffViewerErrors.too_large;
|
||||
},
|
||||
errorMessage() {
|
||||
return this.file.viewer.error_message;
|
||||
return !this.manuallyCollapsed ? this.file.viewer.error_message : '';
|
||||
},
|
||||
forkMessage() {
|
||||
return sprintf(
|
||||
|
@ -85,57 +87,94 @@ export default {
|
|||
false,
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isCollapsed: function fileCollapsedWatch(newVal, oldVal) {
|
||||
if (!newVal && oldVal && !this.hasDiff) {
|
||||
this.handleLoadCollapsedDiff();
|
||||
hasBodyClasses() {
|
||||
const domParts = {
|
||||
header: 'gl-rounded-base!',
|
||||
contentByHash: '',
|
||||
content: '',
|
||||
};
|
||||
|
||||
if (this.showBody) {
|
||||
domParts.header = 'gl-rounded-bottom-left-none gl-rounded-bottom-right-none';
|
||||
domParts.contentByHash =
|
||||
'gl-rounded-none gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-border-1 gl-border-t-0! gl-border-solid gl-border-gray-100';
|
||||
domParts.content = 'gl-rounded-bottom-left-base gl-rounded-bottom-right-base';
|
||||
}
|
||||
|
||||
this.setFileCollapsed({ filePath: this.file.file_path, collapsed: newVal });
|
||||
return domParts;
|
||||
},
|
||||
automaticallyCollapsed() {
|
||||
return collapsedType(this.file) === DIFF_FILE_AUTOMATIC_COLLAPSE;
|
||||
},
|
||||
manuallyCollapsed() {
|
||||
return collapsedType(this.file) === DIFF_FILE_MANUAL_COLLAPSE;
|
||||
},
|
||||
showBody() {
|
||||
return !this.isCollapsed || this.automaticallyCollapsed;
|
||||
},
|
||||
showWarning() {
|
||||
return this.isCollapsed && (this.automaticallyCollapsed && !this.viewDiffsFileByFile);
|
||||
},
|
||||
showContent() {
|
||||
return !this.isCollapsed && !this.isFileTooLarge;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'file.file_hash': {
|
||||
handler: function watchFileHash() {
|
||||
if (this.viewDiffsFileByFile && this.file.viewer.automaticallyCollapsed) {
|
||||
this.isCollapsed = false;
|
||||
this.handleLoadCollapsedDiff();
|
||||
} else {
|
||||
this.isCollapsed = this.file.viewer.automaticallyCollapsed || false;
|
||||
handler: function hashChangeWatch(newHash, oldHash) {
|
||||
this.isCollapsed = isCollapsed(this.file);
|
||||
|
||||
if (newHash && oldHash && !this.hasDiff) {
|
||||
this.requestDiff();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
'file.viewer.automaticallyCollapsed': function setIsCollapsed(newVal) {
|
||||
if (!this.viewDiffsFileByFile) {
|
||||
this.isCollapsed = newVal;
|
||||
}
|
||||
'file.viewer.automaticallyCollapsed': {
|
||||
handler: function autoChangeWatch(automaticValue) {
|
||||
if (collapsedType(this.file) !== DIFF_FILE_MANUAL_COLLAPSE) {
|
||||
this.isCollapsed = this.viewDiffsFileByFile ? false : automaticValue;
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
'file.viewer.manuallyCollapsed': {
|
||||
handler: function manualChangeWatch(manualValue) {
|
||||
if (manualValue !== null) {
|
||||
this.isCollapsed = manualValue;
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
eventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.handleLoadCollapsedDiff);
|
||||
eventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.requestDiff);
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', [
|
||||
'loadCollapsedDiff',
|
||||
'assignDiscussionsToDiff',
|
||||
'setRenderIt',
|
||||
'setFileCollapsed',
|
||||
'setFileCollapsedByUser',
|
||||
]),
|
||||
handleToggle() {
|
||||
if (!this.hasDiff) {
|
||||
this.handleLoadCollapsedDiff();
|
||||
} else {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
this.setRenderIt(this.file);
|
||||
const currentCollapsedFlag = this.isCollapsed;
|
||||
|
||||
this.setFileCollapsedByUser({
|
||||
filePath: this.file.file_path,
|
||||
collapsed: !currentCollapsedFlag,
|
||||
});
|
||||
|
||||
if (!this.hasDiff && currentCollapsedFlag) {
|
||||
this.requestDiff();
|
||||
}
|
||||
},
|
||||
handleLoadCollapsedDiff() {
|
||||
requestDiff() {
|
||||
this.isLoadingCollapsedDiff = true;
|
||||
|
||||
this.loadCollapsedDiff(this.file)
|
||||
.then(() => {
|
||||
this.isLoadingCollapsedDiff = false;
|
||||
this.isCollapsed = false;
|
||||
this.setRenderIt(this.file);
|
||||
})
|
||||
.then(() => {
|
||||
|
@ -167,9 +206,10 @@ export default {
|
|||
:class="{
|
||||
'is-active': currentDiffFileId === file.file_hash,
|
||||
'comments-disabled': Boolean(file.brokenSymlink),
|
||||
'has-body': showBody,
|
||||
}"
|
||||
:data-path="file.new_path"
|
||||
class="diff-file file-holder"
|
||||
class="diff-file file-holder gl-border-none"
|
||||
>
|
||||
<diff-file-header
|
||||
:can-current-user-fork="canCurrentUserFork"
|
||||
|
@ -178,7 +218,8 @@ export default {
|
|||
:expanded="!isCollapsed"
|
||||
:add-merge-request-buttons="true"
|
||||
:view-diffs-file-by-file="viewDiffsFileByFile"
|
||||
class="js-file-title file-title"
|
||||
class="js-file-title file-title gl-border-1 gl-border-solid gl-border-gray-100"
|
||||
:class="hasBodyClasses.header"
|
||||
@toggleFile="handleToggle"
|
||||
@showForkMessage="showForkMessage"
|
||||
/>
|
||||
|
@ -198,21 +239,35 @@ export default {
|
|||
{{ __('Cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
|
||||
<template v-else>
|
||||
<div :id="`diff-content-${file.file_hash}`">
|
||||
<div v-if="errorMessage" class="diff-viewer">
|
||||
<div
|
||||
:id="`diff-content-${file.file_hash}`"
|
||||
:class="hasBodyClasses.contentByHash"
|
||||
data-testid="content-area"
|
||||
>
|
||||
<gl-loading-icon
|
||||
v-if="showLoadingIcon"
|
||||
class="diff-content loading gl-my-0 gl-pt-3"
|
||||
data-testid="loader-icon"
|
||||
/>
|
||||
<div v-else-if="errorMessage" class="diff-viewer">
|
||||
<div v-safe-html="errorMessage" class="nothing-here-block"></div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-show="isCollapsed" class="nothing-here-block diff-collapsed">
|
||||
<div v-show="showWarning" class="nothing-here-block diff-collapsed">
|
||||
{{ __('This diff is collapsed.') }}
|
||||
<a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
|
||||
__('Click to expand it.')
|
||||
}}</a>
|
||||
<a
|
||||
class="click-to-expand"
|
||||
data-testid="toggle-link"
|
||||
href="#"
|
||||
@click.prevent="handleToggle"
|
||||
>
|
||||
{{ __('Click to expand it.') }}
|
||||
</a>
|
||||
</div>
|
||||
<diff-content
|
||||
v-show="!isCollapsed && !isFileTooLarge"
|
||||
v-show="showContent"
|
||||
:class="hasBodyClasses.content"
|
||||
:diff-file="file"
|
||||
:help-page-path="helpPagePath"
|
||||
/>
|
||||
|
|
|
@ -18,6 +18,7 @@ import { __, s__, sprintf } from '~/locale';
|
|||
import { diffViewerModes } from '~/ide/constants';
|
||||
import DiffStats from './diff_stats.vue';
|
||||
import { scrollToElement } from '~/lib/utils/common_utils';
|
||||
import { isCollapsed } from '../diff_file';
|
||||
import { DIFF_FILE_HEADER } from '../i18n';
|
||||
|
||||
export default {
|
||||
|
@ -125,6 +126,9 @@ export default {
|
|||
isUsingLfs() {
|
||||
return this.diffFile.stored_externally && this.diffFile.external_storage === 'lfs';
|
||||
},
|
||||
isCollapsed() {
|
||||
return isCollapsed(this.diffFile, { fileByFile: this.viewDiffsFileByFile });
|
||||
},
|
||||
collapseIcon() {
|
||||
return this.expanded ? 'chevron-down' : 'chevron-right';
|
||||
},
|
||||
|
@ -334,7 +338,7 @@ export default {
|
|||
</gl-dropdown-item>
|
||||
</template>
|
||||
|
||||
<template v-if="!diffFile.viewer.automaticallyCollapsed">
|
||||
<template v-if="!isCollapsed">
|
||||
<gl-dropdown-divider
|
||||
v-if="!diffFile.is_fully_expanded || diffHasDiscussions(diffFile)"
|
||||
/>
|
||||
|
|
|
@ -73,6 +73,10 @@ export const ALERT_OVERFLOW_HIDDEN = 'overflow';
|
|||
export const ALERT_MERGE_CONFLICT = 'merge-conflict';
|
||||
export const ALERT_COLLAPSED_FILES = 'collapsed';
|
||||
|
||||
// Diff File collapse types
|
||||
export const DIFF_FILE_AUTOMATIC_COLLAPSE = 'automatic';
|
||||
export const DIFF_FILE_MANUAL_COLLAPSE = 'manual';
|
||||
|
||||
// State machine states
|
||||
export const STATE_IDLING = 'idle';
|
||||
export const STATE_LOADING = 'loading';
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { DIFF_FILE_SYMLINK_MODE, DIFF_FILE_DELETED_MODE } from './constants';
|
||||
import {
|
||||
DIFF_FILE_SYMLINK_MODE,
|
||||
DIFF_FILE_DELETED_MODE,
|
||||
DIFF_FILE_MANUAL_COLLAPSE,
|
||||
DIFF_FILE_AUTOMATIC_COLLAPSE,
|
||||
} from './constants';
|
||||
|
||||
function fileSymlinkInformation(file, fileList) {
|
||||
const duplicates = fileList.filter(iteratedFile => iteratedFile.file_hash === file.file_hash);
|
||||
|
@ -23,6 +28,7 @@ function collapsed(file) {
|
|||
|
||||
return {
|
||||
automaticallyCollapsed: viewer.automaticallyCollapsed || viewer.collapsed || false,
|
||||
manuallyCollapsed: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -37,3 +43,19 @@ export function prepareRawDiffFile({ file, allFiles }) {
|
|||
|
||||
return file;
|
||||
}
|
||||
|
||||
export function collapsedType(file) {
|
||||
const isManual = typeof file.viewer?.manuallyCollapsed === 'boolean';
|
||||
|
||||
return isManual ? DIFF_FILE_MANUAL_COLLAPSE : DIFF_FILE_AUTOMATIC_COLLAPSE;
|
||||
}
|
||||
|
||||
export function isCollapsed(file) {
|
||||
const type = collapsedType(file);
|
||||
const collapsedStates = {
|
||||
[DIFF_FILE_AUTOMATIC_COLLAPSE]: file.viewer?.automaticallyCollapsed || false,
|
||||
[DIFF_FILE_MANUAL_COLLAPSE]: file.viewer?.manuallyCollapsed,
|
||||
};
|
||||
|
||||
return collapsedStates[type];
|
||||
}
|
||||
|
|
|
@ -40,8 +40,11 @@ import {
|
|||
DIFF_WHITESPACE_COOKIE_NAME,
|
||||
SHOW_WHITESPACE,
|
||||
NO_SHOW_WHITESPACE,
|
||||
DIFF_FILE_MANUAL_COLLAPSE,
|
||||
DIFF_FILE_AUTOMATIC_COLLAPSE,
|
||||
} from '../constants';
|
||||
import { diffViewerModes } from '~/ide/constants';
|
||||
import { isCollapsed } from '../diff_file';
|
||||
|
||||
export const setBaseConfig = ({ commit }, options) => {
|
||||
const {
|
||||
|
@ -239,6 +242,13 @@ export const renderFileForDiscussionId = ({ commit, rootState, state }, discussi
|
|||
if (file.viewer.automaticallyCollapsed) {
|
||||
eventHub.$emit(`loadCollapsedDiff/${file.file_hash}`);
|
||||
scrollToElement(document.getElementById(file.file_hash));
|
||||
} else if (file.viewer.manuallyCollapsed) {
|
||||
commit(types.SET_FILE_COLLAPSED, {
|
||||
filePath: file.file_path,
|
||||
collapsed: false,
|
||||
trigger: DIFF_FILE_AUTOMATIC_COLLAPSE,
|
||||
});
|
||||
eventHub.$emit('scrollToDiscussion');
|
||||
} else {
|
||||
eventHub.$emit('scrollToDiscussion');
|
||||
}
|
||||
|
@ -252,8 +262,7 @@ export const startRenderDiffsQueue = ({ state, commit }) => {
|
|||
const nextFile = state.diffFiles.find(
|
||||
file =>
|
||||
!file.renderIt &&
|
||||
(file.viewer &&
|
||||
(!file.viewer.automaticallyCollapsed || file.viewer.name !== diffViewerModes.text)),
|
||||
(file.viewer && (!isCollapsed(file) || file.viewer.name !== diffViewerModes.text)),
|
||||
);
|
||||
|
||||
if (nextFile) {
|
||||
|
@ -641,8 +650,9 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d
|
|||
});
|
||||
}
|
||||
|
||||
export const setFileCollapsed = ({ commit }, { filePath, collapsed }) =>
|
||||
commit(types.SET_FILE_COLLAPSED, { filePath, collapsed });
|
||||
export const setFileCollapsedByUser = ({ commit }, { filePath, collapsed }) => {
|
||||
commit(types.SET_FILE_COLLAPSED, { filePath, collapsed, trigger: DIFF_FILE_MANUAL_COLLAPSE });
|
||||
};
|
||||
|
||||
export const setSuggestPopoverDismissed = ({ commit, state }) =>
|
||||
axios
|
||||
|
|
|
@ -8,8 +8,16 @@ export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW
|
|||
|
||||
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
|
||||
|
||||
export const hasCollapsedFile = state =>
|
||||
state.diffFiles.some(file => file.viewer && file.viewer.automaticallyCollapsed);
|
||||
export const whichCollapsedTypes = state => {
|
||||
const automatic = state.diffFiles.some(file => file.viewer?.automaticallyCollapsed);
|
||||
const manual = state.diffFiles.some(file => file.viewer?.manuallyCollapsed);
|
||||
|
||||
return {
|
||||
any: automatic || manual,
|
||||
automatic,
|
||||
manual,
|
||||
};
|
||||
};
|
||||
|
||||
export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import Vue from 'vue';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { INLINE_DIFF_VIEW_TYPE } from '../constants';
|
||||
import {
|
||||
DIFF_FILE_MANUAL_COLLAPSE,
|
||||
DIFF_FILE_AUTOMATIC_COLLAPSE,
|
||||
INLINE_DIFF_VIEW_TYPE,
|
||||
} from '../constants';
|
||||
import {
|
||||
findDiffFile,
|
||||
addLineReferences,
|
||||
|
@ -16,6 +20,12 @@ function updateDiffFilesInState(state, files) {
|
|||
return Object.assign(state, { diffFiles: files });
|
||||
}
|
||||
|
||||
function renderFile(file) {
|
||||
Object.assign(file, {
|
||||
renderIt: true,
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
[types.SET_BASE_CONFIG](state, options) {
|
||||
const {
|
||||
|
@ -81,9 +91,7 @@ export default {
|
|||
},
|
||||
|
||||
[types.RENDER_FILE](state, file) {
|
||||
Object.assign(file, {
|
||||
renderIt: true,
|
||||
});
|
||||
renderFile(file);
|
||||
},
|
||||
|
||||
[types.SET_MERGE_REQUEST_DIFFS](state, mergeRequestDiffs) {
|
||||
|
@ -173,6 +181,7 @@ export default {
|
|||
Object.assign(file, {
|
||||
viewer: Object.assign(file.viewer, {
|
||||
automaticallyCollapsed: false,
|
||||
manuallyCollapsed: false,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
@ -351,11 +360,24 @@ export default {
|
|||
file.isShowingFullFile = true;
|
||||
file.isLoadingFullFile = false;
|
||||
},
|
||||
[types.SET_FILE_COLLAPSED](state, { filePath, collapsed }) {
|
||||
[types.SET_FILE_COLLAPSED](
|
||||
state,
|
||||
{ filePath, collapsed, trigger = DIFF_FILE_AUTOMATIC_COLLAPSE },
|
||||
) {
|
||||
const file = state.diffFiles.find(f => f.file_path === filePath);
|
||||
|
||||
if (file && file.viewer) {
|
||||
file.viewer.automaticallyCollapsed = collapsed;
|
||||
if (trigger === DIFF_FILE_MANUAL_COLLAPSE) {
|
||||
file.viewer.automaticallyCollapsed = false;
|
||||
file.viewer.manuallyCollapsed = collapsed;
|
||||
} else if (trigger === DIFF_FILE_AUTOMATIC_COLLAPSE) {
|
||||
file.viewer.automaticallyCollapsed = collapsed;
|
||||
file.viewer.manuallyCollapsed = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (file && !collapsed) {
|
||||
renderFile(file);
|
||||
}
|
||||
},
|
||||
[types.SET_HIDDEN_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
components: {
|
||||
GlIcon,
|
||||
|
@ -45,7 +44,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<button
|
||||
v-tooltip
|
||||
v-gl-tooltip
|
||||
:aria-label="label"
|
||||
:title="tooltipTitle"
|
||||
type="button"
|
||||
|
|
|
@ -5,6 +5,7 @@ import { ApolloLink } from 'apollo-link';
|
|||
import { BatchHttpLink } from 'apollo-link-batch-http';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
|
||||
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
|
||||
|
||||
export const fetchPolicies = {
|
||||
CACHE_FIRST: 'cache-first',
|
||||
|
@ -62,7 +63,7 @@ export default (resolvers = {}, config = {}) => {
|
|||
|
||||
return new ApolloClient({
|
||||
typeDefs: config.typeDefs,
|
||||
link: ApolloLink.from([performanceBarLink, uploadsLink]),
|
||||
link: ApolloLink.from([performanceBarLink, new StartupJSLink(), uploadsLink]),
|
||||
cache: new InMemoryCache({
|
||||
...config.cacheConfig,
|
||||
freezeResults: config.assumeImmutableResults,
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import { ApolloLink, Observable } from 'apollo-link';
|
||||
import { parse } from 'graphql';
|
||||
import { isEqual, pickBy } from 'lodash';
|
||||
|
||||
/**
|
||||
* Remove undefined values from object
|
||||
* @param obj
|
||||
* @returns {Dictionary<unknown>}
|
||||
*/
|
||||
const pickDefinedValues = obj => pickBy(obj, x => x !== undefined);
|
||||
|
||||
/**
|
||||
* Compares two set of variables, order independent
|
||||
*
|
||||
* Ignores undefined values (in the top level) and supports arrays etc.
|
||||
*/
|
||||
const variablesMatch = (var1 = {}, var2 = {}) => {
|
||||
return isEqual(pickDefinedValues(var1), pickDefinedValues(var2));
|
||||
};
|
||||
|
||||
export class StartupJSLink extends ApolloLink {
|
||||
constructor() {
|
||||
super();
|
||||
this.startupCalls = new Map();
|
||||
this.parseStartupCalls(window.gl?.startup_graphql_calls || []);
|
||||
}
|
||||
|
||||
// Extract operationNames from the queries and ensure that we can
|
||||
// match operationName => element from result array
|
||||
parseStartupCalls(calls) {
|
||||
calls.forEach(call => {
|
||||
const { query, variables, fetchCall } = call;
|
||||
const operationName = parse(query)?.definitions?.find(x => x.kind === 'OperationDefinition')
|
||||
?.name?.value;
|
||||
|
||||
if (operationName) {
|
||||
this.startupCalls.set(operationName, {
|
||||
variables,
|
||||
fetchCall,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static noopRequest = (operation, forward) => forward(operation);
|
||||
|
||||
disable() {
|
||||
this.request = StartupJSLink.noopRequest;
|
||||
this.startupCalls = null;
|
||||
}
|
||||
|
||||
request(operation, forward) {
|
||||
// Disable StartupJSLink in case all calls are done or none are set up
|
||||
if (this.startupCalls && this.startupCalls.size === 0) {
|
||||
this.disable();
|
||||
return forward(operation);
|
||||
}
|
||||
|
||||
const { operationName } = operation;
|
||||
|
||||
// Skip startup call if the operationName doesn't match
|
||||
if (!this.startupCalls.has(operationName)) {
|
||||
return forward(operation);
|
||||
}
|
||||
|
||||
const { variables: startupVariables, fetchCall } = this.startupCalls.get(operationName);
|
||||
this.startupCalls.delete(operationName);
|
||||
|
||||
// Skip startup call if the variables values do not match
|
||||
if (!variablesMatch(startupVariables, operation.variables)) {
|
||||
return forward(operation);
|
||||
}
|
||||
|
||||
return new Observable(observer => {
|
||||
fetchCall
|
||||
.then(response => {
|
||||
// Handle HTTP errors
|
||||
if (!response.ok) {
|
||||
throw new Error('fetchCall failed');
|
||||
}
|
||||
operation.setContext({ response });
|
||||
return response.json();
|
||||
})
|
||||
.then(result => {
|
||||
if (result && (result.errors || !result.data)) {
|
||||
throw new Error('Received GraphQL error');
|
||||
}
|
||||
|
||||
// we have data and can send it to back up the link chain
|
||||
observer.next(result);
|
||||
observer.complete();
|
||||
})
|
||||
.catch(() => {
|
||||
forward(operation).subscribe({
|
||||
next: result => {
|
||||
observer.next(result);
|
||||
},
|
||||
error: error => {
|
||||
observer.error(error);
|
||||
},
|
||||
complete: observer.complete.bind(observer),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
|
|||
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
|
||||
import { getDiffMode } from '~/diffs/store/utils';
|
||||
import { diffViewerModes } from '~/ide/constants';
|
||||
import { isCollapsed } from '../../diffs/diff_file';
|
||||
|
||||
const FIRST_CHAR_REGEX = /^(\+|-| )/;
|
||||
|
||||
|
@ -46,6 +47,9 @@ export default {
|
|||
this.discussion.truncated_diff_lines && this.discussion.truncated_diff_lines.length !== 0
|
||||
);
|
||||
},
|
||||
isCollapsed() {
|
||||
return isCollapsed(this.discussion.diff_file);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.isTextFile && !this.hasTruncatedDiffLines) {
|
||||
|
@ -76,7 +80,7 @@ export default {
|
|||
:discussion-path="discussion.discussion_path"
|
||||
:diff-file="discussion.diff_file"
|
||||
:can-current-user-fork="false"
|
||||
:expanded="!discussion.diff_file.viewer.automaticallyCollapsed"
|
||||
:expanded="!isCollapsed"
|
||||
/>
|
||||
<div v-if="isTextFile" class="diff-content">
|
||||
<table class="code js-syntax-highlight" :class="$options.userColorSchemeClass">
|
||||
|
|
|
@ -6,12 +6,12 @@ import {
|
|||
GlDropdownItem,
|
||||
GlIcon,
|
||||
} from '@gitlab/ui';
|
||||
import permissionsQuery from 'shared_queries/repository/permissions.query.graphql';
|
||||
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
|
||||
import { __ } from '../../locale';
|
||||
import getRefMixin from '../mixins/get_ref';
|
||||
import projectShortPathQuery from '../queries/project_short_path.query.graphql';
|
||||
import projectPathQuery from '../queries/project_path.query.graphql';
|
||||
import permissionsQuery from '../queries/permissions.query.graphql';
|
||||
|
||||
const ROW_TYPES = {
|
||||
header: 'header',
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
import filesQuery from 'shared_queries/repository/files.query.graphql';
|
||||
import { deprecatedCreateFlash as createFlash } from '~/flash';
|
||||
import { __ } from '../../locale';
|
||||
import FileTable from './table/index.vue';
|
||||
import getRefMixin from '../mixins/get_ref';
|
||||
import filesQuery from '../queries/files.query.graphql';
|
||||
import projectPathQuery from '../queries/project_path.query.graphql';
|
||||
import FilePreview from './preview/index.vue';
|
||||
import { readmeFile } from '../utils/readme';
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Vue from 'vue';
|
||||
import PathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
|
||||
import { escapeFileUrl } from '../lib/utils/url_utility';
|
||||
import createRouter from './router';
|
||||
import App from './components/app.vue';
|
||||
|
@ -19,10 +18,6 @@ export default function setupVueRepositoryList() {
|
|||
const { dataset } = el;
|
||||
const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset;
|
||||
const router = createRouter(projectPath, escapedRef);
|
||||
const pathRegex = /-\/tree\/[^/]+\/(.+$)/;
|
||||
const matches = window.location.href.match(pathRegex);
|
||||
|
||||
const currentRoutePath = matches ? matches[1] : '';
|
||||
|
||||
apolloProvider.clients.defaultClient.cache.writeData({
|
||||
data: {
|
||||
|
@ -48,28 +43,7 @@ export default function setupVueRepositoryList() {
|
|||
},
|
||||
});
|
||||
|
||||
if (window.gl.startup_graphql_calls) {
|
||||
const query = window.gl.startup_graphql_calls.find(
|
||||
call => call.operationName === 'pathLastCommit',
|
||||
);
|
||||
query.fetchCall
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
apolloProvider.clients.defaultClient.writeQuery({
|
||||
query: PathLastCommitQuery,
|
||||
data: res.data,
|
||||
variables: {
|
||||
projectPath,
|
||||
ref,
|
||||
path: currentRoutePath,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => initLastCommitApp());
|
||||
} else {
|
||||
initLastCommitApp();
|
||||
}
|
||||
initLastCommitApp();
|
||||
|
||||
router.afterEach(({ params: { path } }) => {
|
||||
setTitle(path, ref, fullName);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import filesQuery from '../queries/files.query.graphql';
|
||||
import filesQuery from 'shared_queries/repository/files.query.graphql';
|
||||
import getRefMixin from './get_ref';
|
||||
import projectPathQuery from '../queries/project_path.query.graphql';
|
||||
|
||||
|
|
|
@ -6,11 +6,18 @@
|
|||
border-top: 1px solid $border-color;
|
||||
}
|
||||
|
||||
&.has-body {
|
||||
.file-title {
|
||||
box-shadow: 0 -2px 0 0 var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
table.code tr:last-of-type td:last-of-type {
|
||||
@include gl-rounded-bottom-right-base();
|
||||
}
|
||||
|
||||
.file-title,
|
||||
.file-title-flex-parent {
|
||||
border-top-left-radius: $border-radius-default;
|
||||
border-top-right-radius: $border-radius-default;
|
||||
box-shadow: 0 -2px 0 0 var(--white);
|
||||
cursor: pointer;
|
||||
|
||||
.dropdown-menu {
|
||||
|
@ -113,7 +120,6 @@
|
|||
.diff-content {
|
||||
background: $white;
|
||||
color: $gl-text-color;
|
||||
border-radius: 0 0 3px 3px;
|
||||
|
||||
.unfold {
|
||||
cursor: pointer;
|
||||
|
@ -457,8 +463,11 @@ table.code {
|
|||
border-top: 0;
|
||||
}
|
||||
|
||||
tr:nth-last-of-type(2).line_expansion > td {
|
||||
border-bottom: 0;
|
||||
tr:nth-last-of-type(2).line_expansion,
|
||||
tr:last-of-type.line_expansion {
|
||||
> td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
tr.line_holder td {
|
||||
|
|
|
@ -11,9 +11,19 @@
|
|||
}
|
||||
|
||||
.diff-tree-list {
|
||||
// This 11px value should match the additional value found in
|
||||
// /assets/stylesheets/framework/diffs.scss
|
||||
// for the $mr-file-header-top SCSS variable within the
|
||||
// .file-title,
|
||||
// .file-title-flex-parent {
|
||||
// rule.
|
||||
// If they don't match, the file tree and the diff files stick
|
||||
// to the top at different heights, which is a bad-looking defect
|
||||
$diff-file-header-top: 11px;
|
||||
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + $diff-file-header-top;
|
||||
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 15px;
|
||||
top: $top-pos;
|
||||
max-height: calc(100vh - #{$top-pos});
|
||||
z-index: 202;
|
||||
|
|
|
@ -59,10 +59,17 @@ module Repositories
|
|||
params[:size].to_i
|
||||
end
|
||||
|
||||
def uploaded_file
|
||||
params[:file]
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def store_file!(oid, size)
|
||||
object = LfsObject.find_by(oid: oid, size: size)
|
||||
unless object&.file&.exists?
|
||||
|
||||
if object
|
||||
replace_file!(object) unless object.file&.exists?
|
||||
else
|
||||
object = create_file!(oid, size)
|
||||
end
|
||||
|
||||
|
@ -73,12 +80,19 @@ module Repositories
|
|||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def create_file!(oid, size)
|
||||
uploaded_file = params[:file]
|
||||
return unless uploaded_file.is_a?(UploadedFile)
|
||||
|
||||
LfsObject.create!(oid: oid, size: size, file: uploaded_file)
|
||||
end
|
||||
|
||||
def replace_file!(lfs_object)
|
||||
raise UploadedFile::InvalidPathError unless uploaded_file.is_a?(UploadedFile)
|
||||
|
||||
Gitlab::AppJsonLogger.info(message: "LFS file replaced because it did not exist", oid: oid, size: size)
|
||||
lfs_object.file = uploaded_file
|
||||
lfs_object.save!
|
||||
end
|
||||
|
||||
def link_to_project!(object)
|
||||
return unless object
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ class SearchController < ApplicationController
|
|||
return if check_single_commit_result?
|
||||
|
||||
@search_term = params[:search]
|
||||
@sort = params[:sort] || default_sort
|
||||
|
||||
@scope = search_service.scope
|
||||
@show_snippets = search_service.show_snippets?
|
||||
|
@ -81,6 +82,11 @@ class SearchController < ApplicationController
|
|||
SCOPE_PRELOAD_METHOD[@scope.to_sym]
|
||||
end
|
||||
|
||||
# overridden in EE
|
||||
def default_sort
|
||||
'created_desc'
|
||||
end
|
||||
|
||||
def search_term_valid?
|
||||
unless search_service.valid_query_length?
|
||||
flash[:alert] = t('errors.messages.search_chars_too_long', count: SearchService::SEARCH_CHAR_LIMIT)
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
|
||||
fragment PageInfo on PageInfo {
|
||||
__typename
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
|
||||
fragment TreeEntry on Entry {
|
||||
__typename
|
||||
id
|
||||
sha
|
||||
name
|
||||
|
@ -16,10 +23,15 @@ query getFiles(
|
|||
$nextPageCursor: String
|
||||
) {
|
||||
project(fullPath: $projectPath) {
|
||||
__typename
|
||||
repository {
|
||||
__typename
|
||||
tree(path: $path, ref: $ref) {
|
||||
__typename
|
||||
trees(first: $pageSize, after: $nextPageCursor) {
|
||||
__typename
|
||||
edges {
|
||||
__typename
|
||||
node {
|
||||
...TreeEntry
|
||||
webPath
|
||||
|
@ -30,7 +42,9 @@ query getFiles(
|
|||
}
|
||||
}
|
||||
submodules(first: $pageSize, after: $nextPageCursor) {
|
||||
__typename
|
||||
edges {
|
||||
__typename
|
||||
node {
|
||||
...TreeEntry
|
||||
webUrl
|
||||
|
@ -42,7 +56,9 @@ query getFiles(
|
|||
}
|
||||
}
|
||||
blobs(first: $pageSize, after: $nextPageCursor) {
|
||||
__typename
|
||||
edges {
|
||||
__typename
|
||||
node {
|
||||
...TreeEntry
|
||||
mode
|
|
@ -1,6 +1,8 @@
|
|||
query getPermissions($projectPath: ID!) {
|
||||
project(fullPath: $projectPath) {
|
||||
__typename
|
||||
userPermissions {
|
||||
__typename
|
||||
pushCode
|
||||
forkProject
|
||||
createMergeRequestIn
|
|
@ -30,7 +30,7 @@ module Types
|
|||
if obj.has_action?
|
||||
{
|
||||
button_title: obj.action_button_title,
|
||||
icon: obj.icon,
|
||||
icon: obj.action_icon,
|
||||
method: obj.action_method,
|
||||
path: obj.action_path,
|
||||
title: obj.action_title
|
||||
|
|
|
@ -28,7 +28,8 @@ module SortingHelper
|
|||
sort_value_contacted_date => sort_title_contacted_date,
|
||||
sort_value_relative_position => sort_title_relative_position,
|
||||
sort_value_size => sort_title_size,
|
||||
sort_value_expire_date => sort_title_expire_date
|
||||
sort_value_expire_date => sort_title_expire_date,
|
||||
sort_value_relevant => sort_title_relevant
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -81,6 +82,13 @@ module SortingHelper
|
|||
}
|
||||
end
|
||||
|
||||
def search_reverse_sort_options_hash
|
||||
{
|
||||
sort_value_recently_created => sort_value_oldest_created,
|
||||
sort_value_oldest_created => sort_value_recently_created
|
||||
}
|
||||
end
|
||||
|
||||
def groups_sort_options_hash
|
||||
{
|
||||
sort_value_name => sort_title_name,
|
||||
|
@ -218,6 +226,10 @@ module SortingHelper
|
|||
sort_options_hash[sort_value]
|
||||
end
|
||||
|
||||
def search_sort_option_title(sort_value)
|
||||
sort_options_hash[sort_value]
|
||||
end
|
||||
|
||||
def sort_direction_icon(sort_value)
|
||||
case sort_value
|
||||
when sort_value_milestone, sort_value_due_date, /_asc\z/
|
||||
|
@ -256,6 +268,13 @@ module SortingHelper
|
|||
sort_direction_button(url, reverse_sort, sort_value)
|
||||
end
|
||||
|
||||
def search_sort_direction_button(sort_value)
|
||||
reverse_sort = search_reverse_sort_options_hash[sort_value]
|
||||
url = page_filter_path(sort: reverse_sort)
|
||||
|
||||
sort_direction_button(url, reverse_sort, sort_value)
|
||||
end
|
||||
|
||||
# Titles.
|
||||
def sort_title_access_level_asc
|
||||
s_('SortOptions|Access level, ascending')
|
||||
|
@ -421,6 +440,10 @@ module SortingHelper
|
|||
s_('SortOptions|Expired date')
|
||||
end
|
||||
|
||||
def sort_title_relevant
|
||||
s_('SortOptions|Relevant')
|
||||
end
|
||||
|
||||
# Values.
|
||||
def sort_value_access_level_asc
|
||||
'access_level_asc'
|
||||
|
@ -582,6 +605,10 @@ module SortingHelper
|
|||
'expired_asc'
|
||||
end
|
||||
|
||||
def sort_value_relevant
|
||||
'relevant'
|
||||
end
|
||||
|
||||
def packages_sort_options_hash
|
||||
{
|
||||
sort_value_recently_created => sort_title_created_date,
|
||||
|
|
|
@ -58,10 +58,12 @@ module Emails
|
|||
|
||||
def template_content(email_type)
|
||||
template = Gitlab::Template::ServiceDeskTemplate.find(email_type, @project)
|
||||
|
||||
text = substitute_template_replacements(template.content)
|
||||
|
||||
markdown(text, project: @project)
|
||||
context = { project: @project, pipeline: :email }
|
||||
context[:author] = @note.author if email_type == 'new_note'
|
||||
|
||||
markdown(text, context)
|
||||
rescue Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
.input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url }
|
||||
.input-group-text
|
||||
= root_url
|
||||
= select_tag :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), class: 'select2 js-select-namespace block-truncated', tabindex: 1
|
||||
= select_tag :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), class: 'select2 js-select-namespace block-truncated'
|
||||
- else
|
||||
.input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
|
||||
.input-group-text.border-0
|
||||
|
@ -18,4 +18,4 @@
|
|||
= hidden_field_tag :namespace_id, current_user.namespace_id
|
||||
.form-group.col-12.col-sm-6.project-path
|
||||
= label_tag :path, _('Project slug'), class: 'label-bold'
|
||||
= text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, required: true
|
||||
= text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", required: true
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
= search_field_tag 'search', nil, placeholder: _('Search or jump to…'),
|
||||
class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options',
|
||||
spellcheck: false,
|
||||
tabindex: '1',
|
||||
autocomplete: 'off',
|
||||
data: { issues_path: issues_dashboard_path,
|
||||
mr_path: merge_requests_dashboard_path,
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
};
|
||||
|
||||
gl.startup_graphql_calls = gl.startup_graphql_calls.map(call => ({
|
||||
operationName: call.query.match(/^query (.+)\(/)[1],
|
||||
...call,
|
||||
fetchCall: fetch(url, {
|
||||
...opts,
|
||||
credentials: 'same-origin',
|
||||
|
|
|
@ -26,6 +26,6 @@
|
|||
= render 'shared/ref_dropdown', dropdown_class: 'wide'
|
||||
.form-text.text-muted Existing branch name, tag, or commit SHA
|
||||
.form-actions
|
||||
= button_tag 'Create branch', class: 'gl-button btn btn-success', tabindex: 3
|
||||
= button_tag 'Create branch', class: 'gl-button btn btn-success'
|
||||
= link_to 'Cancel', project_branches_path(@project), class: 'gl-button btn btn-cancel'
|
||||
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
|
||||
|
|
|
@ -16,4 +16,4 @@
|
|||
= render "shared/import_form", f: f
|
||||
|
||||
.form-actions
|
||||
= f.submit 'Start import', class: "gl-button btn btn-success", tabindex: 4
|
||||
= f.submit 'Start import', class: "gl-button btn btn-success"
|
||||
|
|
|
@ -39,5 +39,5 @@
|
|||
= f.check_box :active, required: false, value: @schedule.active?
|
||||
= f.label :active, _('Active'), class: 'gl-font-weight-normal'
|
||||
.footer-block.row-content-block
|
||||
= f.submit _('Save pipeline schedule'), class: 'btn btn-success', tabindex: 3
|
||||
= f.submit _('Save pipeline schedule'), class: 'btn btn-success'
|
||||
= link_to _('Cancel'), pipeline_schedules_path(@project), class: 'btn btn-cancel'
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
= (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe
|
||||
|
||||
.form-actions
|
||||
= f.submit s_('Pipeline|Run Pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3
|
||||
= f.submit s_('Pipeline|Run Pipeline'), class: 'btn btn-success js-variables-save-button'
|
||||
= link_to _('Cancel'), project_pipelines_path(@project), class: 'btn btn-default float-right'
|
||||
|
||||
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
|
||||
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path })
|
||||
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
|
||||
- add_page_startup_graphql_call('repository/permissions', { projectPath: @project.full_path })
|
||||
- add_page_startup_graphql_call('repository/files', { nextPageCursor: "", pageSize: 100, projectPath: @project.full_path, ref: current_ref, path: current_route_path || "/"})
|
||||
- breadcrumb_title _("Repository")
|
||||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
|
||||
|
|
|
@ -3,22 +3,26 @@
|
|||
= render partial: "search/results/empty"
|
||||
= render_if_exists 'shared/promotions/promote_advanced_search'
|
||||
- else
|
||||
.row-content-block.d-md-flex.text-left.align-items-center
|
||||
- unless @search_objects.is_a?(Kaminari::PaginatableWithoutCount)
|
||||
= search_entries_info(@search_objects, @scope, @search_term)
|
||||
- unless @show_snippets
|
||||
- if @project
|
||||
- link_to_project = link_to(@project.full_name, @project, class: 'ml-md-1')
|
||||
- if @scope == 'blobs'
|
||||
= s_("SearchCodeResults|in")
|
||||
.mx-md-1
|
||||
= render partial: "shared/ref_switcher", locals: { ref: repository_ref(@project), form_path: request.fullpath, field_name: 'repository_ref' }
|
||||
= s_('SearchCodeResults|of %{link_to_project}').html_safe % { link_to_project: link_to_project }
|
||||
- else
|
||||
= _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project }
|
||||
- elsif @group
|
||||
- link_to_group = link_to(@group.name, @group, class: 'ml-md-1')
|
||||
= _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
|
||||
.search-results-status
|
||||
.row-content-block.gl-display-flex
|
||||
.gl-display-md-flex.gl-text-left.gl-align-items-center.gl-flex-grow-1
|
||||
- unless @search_objects.is_a?(Kaminari::PaginatableWithoutCount)
|
||||
= search_entries_info(@search_objects, @scope, @search_term)
|
||||
- unless @show_snippets
|
||||
- if @project
|
||||
- link_to_project = link_to(@project.full_name, @project, class: 'ml-md-1')
|
||||
- if @scope == 'blobs'
|
||||
= s_("SearchCodeResults|in")
|
||||
.mx-md-1
|
||||
= render partial: "shared/ref_switcher", locals: { ref: repository_ref(@project), form_path: request.fullpath, field_name: 'repository_ref' }
|
||||
= s_('SearchCodeResults|of %{link_to_project}').html_safe % { link_to_project: link_to_project }
|
||||
- else
|
||||
= _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project }
|
||||
- elsif @group
|
||||
- link_to_group = link_to(@group.name, @group, class: 'ml-md-1')
|
||||
= _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
|
||||
.gl-display-md-flex.gl-flex-direction-column
|
||||
= render partial: 'search/sort_dropdown'
|
||||
= render_if_exists 'shared/promotions/promote_advanced_search'
|
||||
= render partial: "search/results/filters"
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
- return unless ['issues', 'merge_requests'].include?(@scope)
|
||||
|
||||
- sort_value = @sort
|
||||
- sort_title = search_sort_option_title(sort_value)
|
||||
|
||||
.dropdown.gl-display-inline-block.gl-ml-3.filter-dropdown-container
|
||||
.btn-group{ role: 'group' }
|
||||
.btn-group{ role: 'group' }
|
||||
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' }
|
||||
= sort_title
|
||||
= icon('chevron-down')
|
||||
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
|
||||
%li
|
||||
= render_if_exists('search/sort_by_relevancy', sort_title: sort_title)
|
||||
= sortable_item(sort_title_recently_created, page_filter_path(sort: sort_value_recently_created), sort_title)
|
||||
= search_sort_direction_button(sort_value)
|
|
@ -1,2 +1,2 @@
|
|||
= form_tag request.path, method: :get, class: "group-filter-form js-group-filter-form", id: 'group-filter-form' do |f|
|
||||
= search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Search by name'), class: 'group-filter-form-field form-control js-groups-list-filter qa-groups-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2"
|
||||
= search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Search by name'), class: 'group-filter-form-field form-control js-groups-list-filter qa-groups-filter', spellcheck: false, id: 'group-filter-form-field'
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
class: "project-filter-form-field form-control #{form_field_classes}",
|
||||
spellcheck: false,
|
||||
id: 'project-filter-form-field',
|
||||
tabindex: "2",
|
||||
autofocus: local_assigns[:autofocus]
|
||||
|
||||
- if local_assigns[:icon]
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add ability to sort search results for issues and merge requests
|
||||
merge_request: 45003
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove positive tabindexes
|
||||
merge_request: 46003
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Copyedit Project Issue Boards API docs
|
||||
merge_request: 46110
|
||||
author: Takuya Noguchi
|
||||
type: fixed
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Manually collapsed diff files are now significantly shorter and less visually
|
||||
intrusive
|
||||
merge_request: 43911
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Render correct URLs for uploads in service desk issues when custom template
|
||||
is used.
|
||||
merge_request: 45772
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Gracefully recover from deleted LFS file
|
||||
merge_request: 45459
|
||||
author:
|
||||
type: fixed
|
|
@ -4,16 +4,16 @@ group: Project Management
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Issue Boards API
|
||||
# Project Issue Boards API
|
||||
|
||||
Every API call to boards must be authenticated.
|
||||
|
||||
If a user is not a member of a project and the project is private, a `GET`
|
||||
request on that project will result to a `404` status code.
|
||||
|
||||
## Project Board
|
||||
## List project issue boards
|
||||
|
||||
Lists Issue Boards in the given project.
|
||||
Lists project issue boards in the given project.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/boards
|
||||
|
@ -88,9 +88,15 @@ Example response:
|
|||
]
|
||||
```
|
||||
|
||||
## Single board
|
||||
Another example response when no board has been activated or exist in the project:
|
||||
|
||||
Get a single board.
|
||||
```json
|
||||
[]
|
||||
```
|
||||
|
||||
## Show a single issue board
|
||||
|
||||
Get a single project issue board.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/boards/:board_id
|
||||
|
@ -165,9 +171,9 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
## Create a board **(STARTER)**
|
||||
## Create an issue board **(STARTER)**
|
||||
|
||||
Creates a board.
|
||||
Creates a project issue board.
|
||||
|
||||
```plaintext
|
||||
POST /projects/:id/boards
|
||||
|
@ -242,11 +248,11 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
## Update a board **(STARTER)**
|
||||
## Update an issue board **(STARTER)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5954) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.1.
|
||||
|
||||
Updates a board.
|
||||
Updates a project issue board.
|
||||
|
||||
```plaintext
|
||||
PUT /projects/:id/boards/:board_id
|
||||
|
@ -323,9 +329,9 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
## Delete a board **(STARTER)**
|
||||
## Delete an issue board **(STARTER)**
|
||||
|
||||
Deletes a board.
|
||||
Deletes a project issue board.
|
||||
|
||||
```plaintext
|
||||
DELETE /projects/:id/boards/:board_id
|
||||
|
@ -340,10 +346,10 @@ DELETE /projects/:id/boards/:board_id
|
|||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/boards/1"
|
||||
```
|
||||
|
||||
## List board lists
|
||||
## List board lists in a project issue board
|
||||
|
||||
Get a list of the board's lists.
|
||||
Does not include `open` and `closed` lists
|
||||
Does not include `open` and `closed` lists.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/boards/:board_id/lists
|
||||
|
@ -401,7 +407,7 @@ Example response:
|
|||
]
|
||||
```
|
||||
|
||||
## Single board list
|
||||
## Show a single board list
|
||||
|
||||
Get a single board list.
|
||||
|
||||
|
@ -436,9 +442,9 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
## New board list
|
||||
## Create a board list
|
||||
|
||||
Creates a new Issue Board list.
|
||||
Creates a new issue board list.
|
||||
|
||||
```plaintext
|
||||
POST /projects/:id/boards/:board_id/lists
|
||||
|
@ -479,9 +485,9 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
## Edit board list
|
||||
## Reorder a list in a board
|
||||
|
||||
Updates an existing Issue Board list. This call is used to change list position.
|
||||
Updates an existing issue board list. This call is used to change list position.
|
||||
|
||||
```plaintext
|
||||
PUT /projects/:id/boards/:board_id/lists/:list_id
|
||||
|
@ -515,7 +521,7 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
## Delete a board list
|
||||
## Delete a board list from a board
|
||||
|
||||
Only for admins and project owners. Deletes the board list in question.
|
||||
|
||||
|
|
|
@ -356,9 +356,11 @@ Here are the requirements for using dependency scanning in an offline environmen
|
|||
|
||||
- GitLab Runner with the [`docker` or `kubernetes` executor](#requirements).
|
||||
- Docker Container Registry with locally available copies of dependency scanning [analyzer](https://gitlab.com/gitlab-org/security-products/analyzers) images.
|
||||
- Host an offline Git copy of the [gemnasium-db advisory database](https://gitlab.com/gitlab-org/security-products/gemnasium-db/).
|
||||
This is required because, in an offline environment, the Gemnasium analyzer can't fetch the latest
|
||||
advisories from the online repository.
|
||||
- If you have a limited access environment you will need to allow access, such as using a proxy, to the advisory database: `https://gitlab.com/gitlab-org/security-products/gemnasium-db.git`.
|
||||
If you are unable to permit access to `https://gitlab.com/gitlab-org/security-products/gemnasium-db.git` you must host an offline copy of this `git` repository and set the `GEMNASIUM_DB_REMOTE_URL` variable to the URL of this repository. For more information on configuration variables, see [Dependency Scanning](#configuring-dependency-scanning).
|
||||
|
||||
This advisory database is constantly being updated, so you will need to periodically sync your local copy with GitLab's.
|
||||
|
||||
- _Only if scanning Ruby projects_: Host an offline Git copy of the [advisory database](https://github.com/rubysec/ruby-advisory-db).
|
||||
- _Only if scanning npm/yarn projects_: Host an offline copy of the [retire.js](https://github.com/RetireJS/retire.js/) [node](https://github.com/RetireJS/retire.js/blob/master/repository/npmrepository.json) and [js](https://github.com/RetireJS/retire.js/blob/master/repository/jsrepository.json) advisory databases.
|
||||
|
||||
|
|
|
@ -9,15 +9,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
GitLab provides a comprehensive set of features for viewing and managing vulnerabilities:
|
||||
|
||||
- Security dashboards: An overview of the security status in your instance, groups, and projects.
|
||||
- Vulnerability reports: Detailed lists of all vulnerabilities for the instance, group, project, or
|
||||
- Security dashboards: An overview of the security status in your instance, [groups](#group-security-dashboard), and
|
||||
[projects](#project-security-dashboard).
|
||||
- [Vulnerability reports](#vulnerability-report): Detailed lists of all vulnerabilities for the instance, group, project, or
|
||||
pipeline. This is where you triage and manage vulnerabilities.
|
||||
- Security Center: A dedicated area for vulnerability management at the instance level. This
|
||||
- [Security Center](#instance-security-center): A dedicated area for vulnerability management at the instance level. This
|
||||
includes a security dashboard, vulnerability report, and settings.
|
||||
|
||||
You can also drill down into a vulnerability and get extra information. This includes the project it
|
||||
comes from, any related file(s), and metadata that helps you analyze the risk it poses. You can also
|
||||
dismiss a vulnerability or create an issue for it.
|
||||
You can also drill down into a vulnerability and get extra information on the
|
||||
[Vulnerability Page](../vulnerabilities/index.md). This view includes the project it
|
||||
comes from, any related file(s), and metadata that helps you analyze the risk it poses.
|
||||
You can also confirm, dismiss, or resolve a vulnerability, create an issue for it,
|
||||
and in some cases, generate a merge request to fix the vulnerability.
|
||||
|
||||
To benefit from these features, you must first configure one of the
|
||||
[security scanners](../index.md).
|
||||
|
@ -30,7 +33,7 @@ The vulnerability report displays vulnerabilities detected by scanners such as:
|
|||
- [Dynamic Application Security Testing](../dast/index.md)
|
||||
- [Dependency Scanning](../dependency_scanning/index.md)
|
||||
- [Static Application Security Testing](../sast/index.md)
|
||||
- And others!
|
||||
- And [others](../index.md#security-scanning-tools)!
|
||||
|
||||
## Requirements
|
||||
|
||||
|
@ -64,10 +67,10 @@ the analyzer outputs an
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6165) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.1.
|
||||
|
||||
At the project level, the Security Dashboard displays the vulnerabilities merged into your project's
|
||||
At the project level, the Security Dashboard displays the vulnerabilities that exist in your project's
|
||||
[default branch](../../project/repository/branches/index.md#default-branch). Access it by navigating
|
||||
to **Security & Compliance > Security Dashboard**. By default, the Security Dashboard displays all
|
||||
detected and confirmed vulnerabilities.
|
||||
to **Security & Compliance > Security Dashboard**. By default, the Security Dashboard is filtered to
|
||||
display all detected and confirmed vulnerabilities.
|
||||
|
||||
The Security Dashboard first displays the time at which the last pipeline completed on the project's
|
||||
default branch. There's also a link to view this in more detail.
|
||||
|
@ -81,9 +84,11 @@ page to view more information about that vulnerability.
|
|||
|
||||
You can filter the vulnerabilities by one or more of the following:
|
||||
|
||||
- Status
|
||||
- Severity
|
||||
- Scanner
|
||||
| Filter | Available Options |
|
||||
| --- | --- |
|
||||
| Status | Detected, Confirmed, Dismissed, Resolved |
|
||||
| Severity | Critical, High, Medium, Low, Info, Unknown |
|
||||
| Scanner | [Available Scanners](../index.md#security-scanning-tools) |
|
||||
|
||||
You can also dismiss vulnerabilities in the table:
|
||||
|
||||
|
@ -96,7 +101,7 @@ You can also dismiss vulnerabilities in the table:
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6709) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.5.
|
||||
|
||||
The group Security Dashboard gives an overview of the vulnerabilities in the default branches of the
|
||||
The group Security Dashboard gives an overview of the vulnerabilities found in the default branches of the
|
||||
projects in a group and its subgroups. Access it by navigating to **Security > Security Dashboard**
|
||||
after selecting your group. By default, the Security Dashboard displays all detected and confirmed
|
||||
vulnerabilities. If you don't see the vulnerabilities over time graph, the likely cause is that you
|
||||
|
@ -115,11 +120,12 @@ more details about the open vulnerabilities at a specific time.
|
|||
|
||||
Next to the timeline chart is a list of projects, grouped and sorted by the severity of the vulnerability found:
|
||||
|
||||
- F: One or more "critical"
|
||||
- D: One or more "high" or "unknown"
|
||||
- C: One or more "medium"
|
||||
- B: One or more "low"
|
||||
- A: Zero vulnerabilities
|
||||
| Grade | Description |
|
||||
| F | One or more "critical" |
|
||||
| D | One or more "high" or "unknown" |
|
||||
| C | One or more "medium" |
|
||||
| B | One or more "low" |
|
||||
| A | Zero vulnerabilities |
|
||||
|
||||
Projects with no vulnerability tests configured will not appear in the list. Additionally, dismissed
|
||||
vulnerabilities are excluded.
|
||||
|
@ -232,10 +238,12 @@ into the default branch.
|
|||
|
||||
You can filter which vulnerabilities the vulnerability report displays by:
|
||||
|
||||
- Status
|
||||
- Severity
|
||||
- Scanner
|
||||
- Project
|
||||
| Filter | Available Options |
|
||||
| --- | --- |
|
||||
| Status | Detected, Confirmed, Dismissed, Resolved |
|
||||
| Severity | Critical, High, Medium, Low, Info, Unknown |
|
||||
| Scanner | [Available Scanners](../index.md#security-scanning-tools) |
|
||||
| Project | Projects configured in the Security Center settings |
|
||||
|
||||
Clicking any vulnerability in the table takes you to its
|
||||
[Vulnerability Details](../vulnerabilities) page to see more information on that vulnerability.
|
||||
|
|
|
@ -129,12 +129,12 @@ module Gitlab
|
|||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def apply_sort(scope)
|
||||
case sort
|
||||
when 'oldest'
|
||||
when 'created_asc'
|
||||
scope.reorder('created_at ASC')
|
||||
when 'newest'
|
||||
when 'created_desc'
|
||||
scope.reorder('created_at DESC')
|
||||
else
|
||||
scope
|
||||
scope.reorder('created_at DESC')
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
|
|
@ -24949,6 +24949,9 @@ msgstr ""
|
|||
msgid "SortOptions|Recently starred"
|
||||
msgstr ""
|
||||
|
||||
msgid "SortOptions|Relevant"
|
||||
msgstr ""
|
||||
|
||||
msgid "SortOptions|Size"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -127,6 +127,41 @@ RSpec.describe Repositories::LfsStorageController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when existing file has been deleted' do
|
||||
let(:lfs_object) { create(:lfs_object, :with_file) }
|
||||
|
||||
before do
|
||||
FileUtils.rm(lfs_object.file.path)
|
||||
params[:oid] = lfs_object.oid
|
||||
params[:size] = lfs_object.size
|
||||
end
|
||||
|
||||
it 'replaces the file' do
|
||||
expect(Gitlab::AppJsonLogger).to receive(:info).with(message: "LFS file replaced because it did not exist", oid: lfs_object.oid, size: lfs_object.size)
|
||||
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(lfs_object.reload.file).to exist
|
||||
end
|
||||
|
||||
context 'with invalid file' do
|
||||
before do
|
||||
allow_next_instance_of(ActionController::Parameters) do |params|
|
||||
allow(params).to receive(:[]).and_call_original
|
||||
allow(params).to receive(:[]).with(:file).and_return({})
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders LFS forbidden' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
expect(lfs_object.reload.file).not_to exist
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when file is not stored' do
|
||||
it 'renders unprocessable entity' do
|
||||
expect(controller).to receive(:store_file!).and_return(nil)
|
||||
|
|
|
@ -5,8 +5,8 @@ require 'spec_helper'
|
|||
RSpec.describe 'User searches for issues', :js do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, namespace: user.namespace) }
|
||||
let!(:issue1) { create(:issue, title: 'Foo', project: project) }
|
||||
let!(:issue2) { create(:issue, :closed, :confidential, title: 'Bar', project: project) }
|
||||
let!(:issue1) { create(:issue, title: 'issue Foo', project: project, created_at: 1.hour.ago) }
|
||||
let!(:issue2) { create(:issue, :closed, :confidential, title: 'issue Bar', project: project) }
|
||||
|
||||
def search_for_issue(search)
|
||||
fill_in('dashboard_search', with: search)
|
||||
|
@ -67,6 +67,22 @@ RSpec.describe 'User searches for issues', :js do
|
|||
end
|
||||
end
|
||||
|
||||
it 'sorts by created date' do
|
||||
search_for_issue('issue')
|
||||
|
||||
page.within('.results') do
|
||||
expect(page.all('.search-result-row').first).to have_link(issue2.title)
|
||||
expect(page.all('.search-result-row').last).to have_link(issue1.title)
|
||||
end
|
||||
|
||||
find('.reverse-sort-btn').click
|
||||
|
||||
page.within('.results') do
|
||||
expect(page.all('.search-result-row').first).to have_link(issue1.title)
|
||||
expect(page.all('.search-result-row').last).to have_link(issue2.title)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when on a project page' do
|
||||
it 'finds an issue' do
|
||||
find('.js-search-project-dropdown').click
|
||||
|
|
|
@ -697,7 +697,7 @@ describe('diffs/components/app', () => {
|
|||
});
|
||||
|
||||
describe('collapsed files', () => {
|
||||
it('should render the collapsed files warning if there are any collapsed files', () => {
|
||||
it('should render the collapsed files warning if there are any automatically collapsed files', () => {
|
||||
createComponent({}, ({ state }) => {
|
||||
state.diffs.diffFiles = [{ viewer: { automaticallyCollapsed: true } }];
|
||||
});
|
||||
|
@ -705,16 +705,14 @@ describe('diffs/components/app', () => {
|
|||
expect(getCollapsedFilesWarning(wrapper).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not render the collapsed files warning if the user has dismissed the alert already', async () => {
|
||||
it('should not render the collapsed files warning if there are no automatically collapsed files', () => {
|
||||
createComponent({}, ({ state }) => {
|
||||
state.diffs.diffFiles = [{ viewer: { automaticallyCollapsed: true } }];
|
||||
state.diffs.diffFiles = [
|
||||
{ viewer: { automaticallyCollapsed: false, manuallyCollapsed: true } },
|
||||
{ viewer: { automaticallyCollapsed: false, manuallyCollapsed: false } },
|
||||
];
|
||||
});
|
||||
|
||||
expect(getCollapsedFilesWarning(wrapper).exists()).toBe(true);
|
||||
|
||||
wrapper.vm.collapsedWarningDismissed = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(getCollapsedFilesWarning(wrapper).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
|
||||
|
||||
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import diffDiscussionsMockData from '../mock_data/diff_discussions';
|
||||
|
@ -136,9 +139,25 @@ describe('DiffFileHeader component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('displays a copy to clipboard button', () => {
|
||||
createComponent();
|
||||
expect(wrapper.find(ClipboardButton).exists()).toBe(true);
|
||||
describe('copy to clipboard', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('displays a copy to clipboard button', () => {
|
||||
expect(wrapper.find(ClipboardButton).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('triggers the copy to clipboard tracking event', () => {
|
||||
const trackingSpy = mockTracking('_category_', wrapper.vm.$el, jest.spyOn);
|
||||
|
||||
triggerEvent('[data-testid="diff-file-copy-clipboard"]');
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_copy_file_button', {
|
||||
label: 'diff_copy_file_path_button',
|
||||
property: 'diff_copy_file',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for submodule', () => {
|
||||
|
|
|
@ -1,262 +1,317 @@
|
|||
import Vue from 'vue';
|
||||
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
|
||||
import { createStore } from '~/mr_notes/stores';
|
||||
import DiffFileComponent from '~/diffs/components/diff_file.vue';
|
||||
import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
|
||||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import createDiffsStore from '~/diffs/store/modules';
|
||||
import diffFileMockDataReadable from '../mock_data/diff_file';
|
||||
import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable';
|
||||
|
||||
describe('DiffFile', () => {
|
||||
let vm;
|
||||
let trackingSpy;
|
||||
import DiffFileComponent from '~/diffs/components/diff_file.vue';
|
||||
import DiffFileHeaderComponent from '~/diffs/components/diff_file_header.vue';
|
||||
import DiffContentComponent from '~/diffs/components/diff_content.vue';
|
||||
|
||||
beforeEach(() => {
|
||||
vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), {
|
||||
file: JSON.parse(JSON.stringify(diffFileMockDataReadable)),
|
||||
import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
|
||||
|
||||
function changeViewer(store, index, { automaticallyCollapsed, manuallyCollapsed, name }) {
|
||||
const file = store.state.diffs.diffFiles[index];
|
||||
const newViewer = {
|
||||
...file.viewer,
|
||||
};
|
||||
|
||||
if (automaticallyCollapsed !== undefined) {
|
||||
newViewer.automaticallyCollapsed = automaticallyCollapsed;
|
||||
}
|
||||
|
||||
if (manuallyCollapsed !== undefined) {
|
||||
newViewer.manuallyCollapsed = manuallyCollapsed;
|
||||
}
|
||||
|
||||
if (name !== undefined) {
|
||||
newViewer.name = name;
|
||||
}
|
||||
|
||||
Object.assign(file, {
|
||||
viewer: newViewer,
|
||||
});
|
||||
}
|
||||
|
||||
function forceHasDiff({ store, index = 0, inlineLines, parallelLines, readableText }) {
|
||||
const file = store.state.diffs.diffFiles[index];
|
||||
|
||||
Object.assign(file, {
|
||||
highlighted_diff_lines: inlineLines,
|
||||
parallel_diff_lines: parallelLines,
|
||||
blob: {
|
||||
...file.blob,
|
||||
readable_text: readableText,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function markFileToBeRendered(store, index = 0) {
|
||||
const file = store.state.diffs.diffFiles[index];
|
||||
|
||||
Object.assign(file, {
|
||||
renderIt: true,
|
||||
});
|
||||
}
|
||||
|
||||
function createComponent({ file }) {
|
||||
const localVue = createLocalVue();
|
||||
|
||||
localVue.use(Vuex);
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
diffs: createDiffsStore(),
|
||||
},
|
||||
});
|
||||
|
||||
store.state.diffs.diffFiles = [file];
|
||||
|
||||
const wrapper = shallowMount(DiffFileComponent, {
|
||||
store,
|
||||
localVue,
|
||||
propsData: {
|
||||
file,
|
||||
canCurrentUserFork: false,
|
||||
viewDiffsFileByFile: false,
|
||||
}).$mount();
|
||||
trackingSpy = mockTracking('_category_', vm.$el, jest.spyOn);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
localVue,
|
||||
wrapper,
|
||||
store,
|
||||
};
|
||||
}
|
||||
|
||||
const findDiffHeader = wrapper => wrapper.find(DiffFileHeaderComponent);
|
||||
const findDiffContentArea = wrapper => wrapper.find('[data-testid="content-area"]');
|
||||
const findLoader = wrapper => wrapper.find('[data-testid="loader-icon"]');
|
||||
const findToggleLinks = wrapper => wrapper.findAll('[data-testid="toggle-link"]');
|
||||
|
||||
const toggleFile = wrapper => findDiffHeader(wrapper).vm.$emit('toggleFile');
|
||||
const isDisplayNone = element => element.style.display === 'none';
|
||||
const getReadableFile = () => JSON.parse(JSON.stringify(diffFileMockDataReadable));
|
||||
const getUnreadableFile = () => JSON.parse(JSON.stringify(diffFileMockDataUnreadable));
|
||||
|
||||
const makeFileAutomaticallyCollapsed = (store, index = 0) =>
|
||||
changeViewer(store, index, { automaticallyCollapsed: true, manuallyCollapsed: null });
|
||||
const makeFileOpenByDefault = (store, index = 0) =>
|
||||
changeViewer(store, index, { automaticallyCollapsed: false, manuallyCollapsed: null });
|
||||
const makeFileManuallyCollapsed = (store, index = 0) =>
|
||||
changeViewer(store, index, { automaticallyCollapsed: false, manuallyCollapsed: true });
|
||||
const changeViewerType = (store, newType, index = 0) =>
|
||||
changeViewer(store, index, { name: diffViewerModes[newType] });
|
||||
|
||||
describe('DiffFile', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
({ wrapper, store } = createComponent({ file: getReadableFile() }));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findDiffContent = () => vm.$el.querySelector('.diff-content');
|
||||
const isVisible = el => el.style.display !== 'none';
|
||||
|
||||
describe('template', () => {
|
||||
it('should render component with file header, file content components', done => {
|
||||
const el = vm.$el;
|
||||
const { file_hash, file_path } = vm.file;
|
||||
it('should render component with file header, file content components', async () => {
|
||||
const el = wrapper.vm.$el;
|
||||
const { file_hash } = wrapper.vm.file;
|
||||
|
||||
expect(el.id).toEqual(file_hash);
|
||||
expect(el.classList.contains('diff-file')).toEqual(true);
|
||||
|
||||
expect(el.querySelectorAll('.diff-content.hidden').length).toEqual(0);
|
||||
expect(el.querySelector('.js-file-title')).toBeDefined();
|
||||
expect(el.querySelector('[data-testid="diff-file-copy-clipboard"]')).toBeDefined();
|
||||
expect(el.querySelector('.file-title-name').innerText.indexOf(file_path)).toBeGreaterThan(-1);
|
||||
expect(wrapper.find(DiffFileHeaderComponent).exists()).toBe(true);
|
||||
expect(el.querySelector('.js-syntax-highlight')).toBeDefined();
|
||||
|
||||
vm.file.renderIt = true;
|
||||
markFileToBeRendered(store);
|
||||
|
||||
vm.$nextTick()
|
||||
.then(() => {
|
||||
expect(el.querySelectorAll('.line_content').length).toBe(8);
|
||||
expect(el.querySelectorAll('.js-line-expansion-content').length).toBe(1);
|
||||
triggerEvent('[data-testid="diff-file-copy-clipboard"]');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find(DiffContentComponent).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collapsing', () => {
|
||||
describe('user collapsed', () => {
|
||||
beforeEach(() => {
|
||||
makeFileManuallyCollapsed(store);
|
||||
});
|
||||
|
||||
it('should not have any content at all', async () => {
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
Array.from(findDiffContentArea(wrapper).element.children).forEach(child => {
|
||||
expect(isDisplayNone(child)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not have the class `has-body` to present the header differently', () => {
|
||||
expect(wrapper.classes('has-body')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should track a click event on copy to clip board button', done => {
|
||||
const el = vm.$el;
|
||||
describe('automatically collapsed', () => {
|
||||
beforeEach(() => {
|
||||
makeFileAutomaticallyCollapsed(store);
|
||||
});
|
||||
|
||||
expect(el.querySelector('[data-testid="diff-file-copy-clipboard"]')).toBeDefined();
|
||||
vm.file.renderIt = true;
|
||||
vm.$nextTick()
|
||||
.then(() => {
|
||||
triggerEvent('[data-testid="diff-file-copy-clipboard"]');
|
||||
it('should show the collapsed file warning with expansion link', () => {
|
||||
expect(findDiffContentArea(wrapper).html()).toContain('This diff is collapsed');
|
||||
expect(findToggleLinks(wrapper).length).toEqual(1);
|
||||
});
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_copy_file_button', {
|
||||
label: 'diff_copy_file_path_button',
|
||||
property: 'diff_copy_file',
|
||||
});
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
it('should style the component so that it `.has-body` for layout purposes', () => {
|
||||
expect(wrapper.classes('has-body')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collapsed', () => {
|
||||
it('should not have file content', done => {
|
||||
expect(isVisible(findDiffContent())).toBe(true);
|
||||
expect(vm.isCollapsed).toEqual(false);
|
||||
vm.isCollapsed = true;
|
||||
vm.file.renderIt = true;
|
||||
describe('not collapsed', () => {
|
||||
beforeEach(() => {
|
||||
makeFileOpenByDefault(store);
|
||||
markFileToBeRendered(store);
|
||||
});
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(isVisible(findDiffContent())).toBe(false);
|
||||
it('should have the file content', async () => {
|
||||
expect(wrapper.find(DiffContentComponent).exists()).toBe(true);
|
||||
});
|
||||
|
||||
done();
|
||||
it('should style the component so that it `.has-body` for layout purposes', () => {
|
||||
expect(wrapper.classes('has-body')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggle', () => {
|
||||
it('should update store state', async () => {
|
||||
jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation(() => {});
|
||||
|
||||
toggleFile(wrapper);
|
||||
|
||||
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/setFileCollapsedByUser', {
|
||||
filePath: wrapper.vm.file.file_path,
|
||||
collapsed: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should have collapsed text and link', done => {
|
||||
vm.renderIt = true;
|
||||
vm.isCollapsed = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.innerText).toContain('This diff is collapsed');
|
||||
expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have collapsed text and link even before rendered', done => {
|
||||
vm.renderIt = false;
|
||||
vm.isCollapsed = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.innerText).toContain('This diff is collapsed');
|
||||
expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be collapsable for unreadable files', done => {
|
||||
vm.$destroy();
|
||||
vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), {
|
||||
file: JSON.parse(JSON.stringify(diffFileMockDataUnreadable)),
|
||||
canCurrentUserFork: false,
|
||||
viewDiffsFileByFile: false,
|
||||
}).$mount();
|
||||
|
||||
vm.renderIt = false;
|
||||
vm.isCollapsed = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.innerText).toContain('This diff is collapsed');
|
||||
expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be collapsed for renamed files', done => {
|
||||
vm.renderIt = true;
|
||||
vm.isCollapsed = false;
|
||||
vm.file.highlighted_diff_lines = null;
|
||||
vm.file.viewer.name = diffViewerModes.renamed;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.innerText).not.toContain('This diff is collapsed');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be collapsed for mode changed files', done => {
|
||||
vm.renderIt = true;
|
||||
vm.isCollapsed = false;
|
||||
vm.file.highlighted_diff_lines = null;
|
||||
vm.file.viewer.name = diffViewerModes.mode_changed;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.innerText).not.toContain('This diff is collapsed');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have loading icon while loading a collapsed diffs', done => {
|
||||
vm.isCollapsed = true;
|
||||
vm.isLoadingCollapsedDiff = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelectorAll('.diff-content.loading').length).toEqual(1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should update store state', done => {
|
||||
jest.spyOn(vm.$store, 'dispatch').mockImplementation(() => {});
|
||||
|
||||
vm.isCollapsed = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/setFileCollapsed', {
|
||||
filePath: vm.file.file_path,
|
||||
collapsed: true,
|
||||
describe('fetch collapsed diff', () => {
|
||||
const prepFile = async (inlineLines, parallelLines, readableText) => {
|
||||
forceHasDiff({
|
||||
store,
|
||||
inlineLines,
|
||||
parallelLines,
|
||||
readableText,
|
||||
});
|
||||
|
||||
done();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
toggleFile(wrapper);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(wrapper.vm, 'requestDiff').mockImplementation(() => {});
|
||||
|
||||
makeFileAutomaticallyCollapsed(store);
|
||||
});
|
||||
|
||||
it.each`
|
||||
inlineLines | parallelLines | readableText
|
||||
${[1]} | ${[1]} | ${true}
|
||||
${[]} | ${[1]} | ${true}
|
||||
${[1]} | ${[]} | ${true}
|
||||
${[1]} | ${[1]} | ${false}
|
||||
${[]} | ${[]} | ${false}
|
||||
`(
|
||||
'does not make a request to fetch the diff for a diff file like { inline: $inlineLines, parallel: $parallelLines, readableText: $readableText }',
|
||||
async ({ inlineLines, parallelLines, readableText }) => {
|
||||
await prepFile(inlineLines, parallelLines, readableText);
|
||||
|
||||
expect(wrapper.vm.requestDiff).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
inlineLines | parallelLines | readableText
|
||||
${[]} | ${[]} | ${true}
|
||||
`(
|
||||
'makes a request to fetch the diff for a diff file like { inline: $inlineLines, parallel: $parallelLines, readableText: $readableText }',
|
||||
async ({ inlineLines, parallelLines, readableText }) => {
|
||||
await prepFile(inlineLines, parallelLines, readableText);
|
||||
|
||||
expect(wrapper.vm.requestDiff).toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loading', () => {
|
||||
it('should have loading icon while loading a collapsed diffs', async () => {
|
||||
makeFileAutomaticallyCollapsed(store);
|
||||
wrapper.vm.isLoadingCollapsedDiff = true;
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findLoader(wrapper).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('general (other) collapsed', () => {
|
||||
it('should be expandable for unreadable files', async () => {
|
||||
({ wrapper, store } = createComponent({ file: getUnreadableFile() }));
|
||||
makeFileAutomaticallyCollapsed(store);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findDiffContentArea(wrapper).html()).toContain('This diff is collapsed');
|
||||
expect(findToggleLinks(wrapper).length).toEqual(1);
|
||||
});
|
||||
|
||||
it('updates local state when changing file state', done => {
|
||||
vm.file.viewer.automaticallyCollapsed = true;
|
||||
it.each`
|
||||
mode
|
||||
${'renamed'}
|
||||
${'mode_changed'}
|
||||
`(
|
||||
'should render the DiffContent component for files whose mode is $mode',
|
||||
async ({ mode }) => {
|
||||
makeFileOpenByDefault(store);
|
||||
markFileToBeRendered(store);
|
||||
changeViewerType(store, mode);
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.isCollapsed).toBe(true);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
expect(wrapper.classes('has-body')).toBe(true);
|
||||
expect(wrapper.find(DiffContentComponent).exists()).toBe(true);
|
||||
expect(wrapper.find(DiffContentComponent).isVisible()).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('too large diff', () => {
|
||||
it('should have too large warning and blob link', done => {
|
||||
it('should have too large warning and blob link', async () => {
|
||||
const file = store.state.diffs.diffFiles[0];
|
||||
const BLOB_LINK = '/file/view/path';
|
||||
vm.file.viewer.error = diffViewerErrors.too_large;
|
||||
vm.file.viewer.error_message =
|
||||
'This source diff could not be displayed because it is too large';
|
||||
vm.file.view_path = BLOB_LINK;
|
||||
vm.file.renderIt = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.innerText).toContain(
|
||||
'This source diff could not be displayed because it is too large',
|
||||
);
|
||||
|
||||
done();
|
||||
Object.assign(store.state.diffs.diffFiles[0], {
|
||||
...file,
|
||||
view_path: BLOB_LINK,
|
||||
renderIt: true,
|
||||
viewer: {
|
||||
...file.viewer,
|
||||
error: diffViewerErrors.too_large,
|
||||
error_message: 'This source diff could not be displayed because it is too large',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('watch collapsed', () => {
|
||||
it('calls handleLoadCollapsedDiff if collapsed changed & file has no lines', done => {
|
||||
jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {});
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
vm.file.highlighted_diff_lines = [];
|
||||
vm.file.parallel_diff_lines = [];
|
||||
vm.isCollapsed = true;
|
||||
|
||||
vm.$nextTick()
|
||||
.then(() => {
|
||||
vm.isCollapsed = false;
|
||||
|
||||
return vm.$nextTick();
|
||||
})
|
||||
.then(() => {
|
||||
expect(vm.handleLoadCollapsedDiff).toHaveBeenCalled();
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('does not call handleLoadCollapsedDiff if collapsed changed & file is unreadable', done => {
|
||||
vm.$destroy();
|
||||
vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), {
|
||||
file: JSON.parse(JSON.stringify(diffFileMockDataUnreadable)),
|
||||
canCurrentUserFork: false,
|
||||
viewDiffsFileByFile: false,
|
||||
}).$mount();
|
||||
|
||||
jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {});
|
||||
|
||||
vm.file.highlighted_diff_lines = [];
|
||||
vm.file.parallel_diff_lines = undefined;
|
||||
vm.isCollapsed = true;
|
||||
|
||||
vm.$nextTick()
|
||||
.then(() => {
|
||||
vm.isCollapsed = false;
|
||||
|
||||
return vm.$nextTick();
|
||||
})
|
||||
.then(() => {
|
||||
expect(vm.handleLoadCollapsedDiff).not.toHaveBeenCalled();
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
expect(wrapper.vm.$el.innerText).toContain(
|
||||
'This source diff could not be displayed because it is too large',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ export default {
|
|||
name: 'text',
|
||||
error: null,
|
||||
automaticallyCollapsed: false,
|
||||
manuallyCollapsed: null,
|
||||
},
|
||||
added_lines: 2,
|
||||
removed_lines: 0,
|
||||
|
|
|
@ -26,6 +26,7 @@ export default {
|
|||
name: 'text',
|
||||
error: null,
|
||||
automaticallyCollapsed: false,
|
||||
manuallyCollapsed: null,
|
||||
},
|
||||
added_lines: 0,
|
||||
removed_lines: 0,
|
||||
|
|
|
@ -42,7 +42,7 @@ import {
|
|||
fetchFullDiff,
|
||||
toggleFullDiff,
|
||||
switchToFullDiffFromRenamedFile,
|
||||
setFileCollapsed,
|
||||
setFileCollapsedByUser,
|
||||
setExpandedDiffLines,
|
||||
setSuggestPopoverDismissed,
|
||||
changeCurrentCommit,
|
||||
|
@ -1216,13 +1216,18 @@ describe('DiffsStoreActions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('setFileCollapsed', () => {
|
||||
describe('setFileUserCollapsed', () => {
|
||||
it('commits SET_FILE_COLLAPSED', done => {
|
||||
testAction(
|
||||
setFileCollapsed,
|
||||
setFileCollapsedByUser,
|
||||
{ filePath: 'test', collapsed: true },
|
||||
null,
|
||||
[{ type: types.SET_FILE_COLLAPSED, payload: { filePath: 'test', collapsed: true } }],
|
||||
[
|
||||
{
|
||||
type: types.SET_FILE_COLLAPSED,
|
||||
payload: { filePath: 'test', collapsed: true, trigger: 'manual' },
|
||||
},
|
||||
],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
|
|
|
@ -49,23 +49,53 @@ describe('Diffs Module Getters', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('hasCollapsedFile', () => {
|
||||
it('returns true when all files are collapsed', () => {
|
||||
localState.diffFiles = [
|
||||
{ viewer: { automaticallyCollapsed: true } },
|
||||
{ viewer: { automaticallyCollapsed: true } },
|
||||
];
|
||||
describe('whichCollapsedTypes', () => {
|
||||
const autoCollapsedFile = { viewer: { automaticallyCollapsed: true, manuallyCollapsed: null } };
|
||||
const manuallyCollapsedFile = {
|
||||
viewer: { automaticallyCollapsed: false, manuallyCollapsed: true },
|
||||
};
|
||||
const openFile = { viewer: { automaticallyCollapsed: false, manuallyCollapsed: false } };
|
||||
|
||||
expect(getters.hasCollapsedFile(localState)).toEqual(true);
|
||||
it.each`
|
||||
description | value | files
|
||||
${'all files are automatically collapsed'} | ${true} | ${[{ ...autoCollapsedFile }, { ...autoCollapsedFile }]}
|
||||
${'all files are manually collapsed'} | ${true} | ${[{ ...manuallyCollapsedFile }, { ...manuallyCollapsedFile }]}
|
||||
${'no files are collapsed in any way'} | ${false} | ${[{ ...openFile }, { ...openFile }]}
|
||||
${'some files are collapsed in either way'} | ${true} | ${[{ ...manuallyCollapsedFile }, { ...autoCollapsedFile }, { ...openFile }]}
|
||||
`('`any` is $value when $description', ({ value, files }) => {
|
||||
localState.diffFiles = files;
|
||||
|
||||
const getterResult = getters.whichCollapsedTypes(localState);
|
||||
|
||||
expect(getterResult.any).toEqual(value);
|
||||
});
|
||||
|
||||
it('returns true when at least one file is collapsed', () => {
|
||||
localState.diffFiles = [
|
||||
{ viewer: { automaticallyCollapsed: false } },
|
||||
{ viewer: { automaticallyCollapsed: true } },
|
||||
];
|
||||
it.each`
|
||||
description | value | files
|
||||
${'all files are automatically collapsed'} | ${true} | ${[{ ...autoCollapsedFile }, { ...autoCollapsedFile }]}
|
||||
${'all files are manually collapsed'} | ${false} | ${[{ ...manuallyCollapsedFile }, { ...manuallyCollapsedFile }]}
|
||||
${'no files are collapsed in any way'} | ${false} | ${[{ ...openFile }, { ...openFile }]}
|
||||
${'some files are collapsed in either way'} | ${true} | ${[{ ...manuallyCollapsedFile }, { ...autoCollapsedFile }, { ...openFile }]}
|
||||
`('`automatic` is $value when $description', ({ value, files }) => {
|
||||
localState.diffFiles = files;
|
||||
|
||||
expect(getters.hasCollapsedFile(localState)).toEqual(true);
|
||||
const getterResult = getters.whichCollapsedTypes(localState);
|
||||
|
||||
expect(getterResult.automatic).toEqual(value);
|
||||
});
|
||||
|
||||
it.each`
|
||||
description | value | files
|
||||
${'all files are automatically collapsed'} | ${false} | ${[{ ...autoCollapsedFile }, { ...autoCollapsedFile }]}
|
||||
${'all files are manually collapsed'} | ${true} | ${[{ ...manuallyCollapsedFile }, { ...manuallyCollapsedFile }]}
|
||||
${'no files are collapsed in any way'} | ${false} | ${[{ ...openFile }, { ...openFile }]}
|
||||
${'some files are collapsed in either way'} | ${true} | ${[{ ...manuallyCollapsedFile }, { ...autoCollapsedFile }, { ...openFile }]}
|
||||
`('`manual` is $value when $description', ({ value, files }) => {
|
||||
localState.diffFiles = files;
|
||||
|
||||
const getterResult = getters.whichCollapsedTypes(localState);
|
||||
|
||||
expect(getterResult.manual).toEqual(value);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,375 @@
|
|||
import { ApolloLink, Observable } from 'apollo-link';
|
||||
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
|
||||
|
||||
describe('StartupJSLink', () => {
|
||||
const FORWARDED_RESPONSE = { data: 'FORWARDED_RESPONSE' };
|
||||
|
||||
const STARTUP_JS_RESPONSE = { data: 'STARTUP_JS_RESPONSE' };
|
||||
const OPERATION_NAME = 'startupJSQuery';
|
||||
const STARTUP_JS_QUERY = `query ${OPERATION_NAME}($id: Int = 3){
|
||||
name
|
||||
id
|
||||
}`;
|
||||
|
||||
const STARTUP_JS_RESPONSE_TWO = { data: 'STARTUP_JS_RESPONSE_TWO' };
|
||||
const OPERATION_NAME_TWO = 'startupJSQueryTwo';
|
||||
const STARTUP_JS_QUERY_TWO = `query ${OPERATION_NAME_TWO}($id: Int = 3){
|
||||
id
|
||||
name
|
||||
}`;
|
||||
|
||||
const ERROR_RESPONSE = {
|
||||
data: {
|
||||
user: null,
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
path: ['user'],
|
||||
locations: [{ line: 2, column: 3 }],
|
||||
extensions: {
|
||||
message: 'Object not found',
|
||||
type: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let startupLink;
|
||||
let link;
|
||||
|
||||
function mockFetchCall(status = 200, response = STARTUP_JS_RESPONSE) {
|
||||
const p = {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||
statusText: `MOCK-FETCH ${status}`,
|
||||
clone: () => p,
|
||||
json: () => Promise.resolve(response),
|
||||
};
|
||||
return Promise.resolve(p);
|
||||
}
|
||||
|
||||
function mockOperation({ operationName = OPERATION_NAME, variables = { id: 3 } } = {}) {
|
||||
return { operationName, variables, setContext: () => {} };
|
||||
}
|
||||
|
||||
const setupLink = () => {
|
||||
startupLink = new StartupJSLink();
|
||||
link = ApolloLink.from([startupLink, new ApolloLink(() => Observable.of(FORWARDED_RESPONSE))]);
|
||||
};
|
||||
|
||||
it('forwards requests if no calls are set up', done => {
|
||||
setupLink();
|
||||
link.request(mockOperation()).subscribe(result => {
|
||||
expect(result).toEqual(FORWARDED_RESPONSE);
|
||||
expect(startupLink.startupCalls).toBe(null);
|
||||
expect(startupLink.request).toEqual(StartupJSLink.noopRequest);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards requests if the operation is not pre-loaded', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { id: 3 },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation({ operationName: 'notLoaded' })).subscribe(result => {
|
||||
expect(result).toEqual(FORWARDED_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('variable match errors: ', () => {
|
||||
it('forwards requests if the variables are not matching', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { id: 'NOT_MATCHING' },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation()).subscribe(result => {
|
||||
expect(result).toEqual(FORWARDED_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards requests if more variables are set in the operation', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(),
|
||||
query: STARTUP_JS_QUERY,
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation()).subscribe(result => {
|
||||
expect(result).toEqual(FORWARDED_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards requests if less variables are set in the operation', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { id: 3, name: 'tanuki' },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation({ variables: { id: 3 } })).subscribe(result => {
|
||||
expect(result).toEqual(FORWARDED_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards requests if different variables are set', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { name: 'tanuki' },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation({ variables: { id: 3 } })).subscribe(result => {
|
||||
expect(result).toEqual(FORWARDED_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards requests if array variables have a different order', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { id: [3, 4] },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation({ variables: { id: [4, 3] } })).subscribe(result => {
|
||||
expect(result).toEqual(FORWARDED_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('forwards the call if the fetchCall is failing with a HTTP Error', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(404),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { id: 3 },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation()).subscribe(result => {
|
||||
expect(result).toEqual(FORWARDED_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards the call if it errors (e.g. failing JSON)', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: Promise.reject(new Error('Parsing failed')),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { id: 3 },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation()).subscribe(result => {
|
||||
expect(result).toEqual(FORWARDED_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards the call if the response contains an error', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(200, ERROR_RESPONSE),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { id: 3 },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation()).subscribe(result => {
|
||||
expect(result).toEqual(FORWARDED_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards the call if the response doesn't contain a data object", done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(200, { 'no-data': 'yay' }),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { id: 3 },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation()).subscribe(result => {
|
||||
expect(result).toEqual(FORWARDED_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves the request if the operation is matching', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { id: 3 },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation()).subscribe(result => {
|
||||
expect(result).toEqual(STARTUP_JS_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves the request exactly once', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { id: 3 },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation()).subscribe(result => {
|
||||
expect(result).toEqual(STARTUP_JS_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
link.request(mockOperation()).subscribe(result2 => {
|
||||
expect(result2).toEqual(FORWARDED_RESPONSE);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves the request if the variables have a different order', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { id: 3, name: 'foo' },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation({ variables: { name: 'foo', id: 3 } })).subscribe(result => {
|
||||
expect(result).toEqual(STARTUP_JS_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves the request if the variables have undefined values', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { name: 'foo' },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link
|
||||
.request(mockOperation({ variables: { name: 'foo', undef: undefined } }))
|
||||
.subscribe(result => {
|
||||
expect(result).toEqual(STARTUP_JS_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves the request if the variables are of an array format', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { id: [3, 4] },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation({ variables: { id: [3, 4] } })).subscribe(result => {
|
||||
expect(result).toEqual(STARTUP_JS_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves multiple requests correctly', done => {
|
||||
window.gl = {
|
||||
startup_graphql_calls: [
|
||||
{
|
||||
fetchCall: mockFetchCall(),
|
||||
query: STARTUP_JS_QUERY,
|
||||
variables: { id: 3 },
|
||||
},
|
||||
{
|
||||
fetchCall: mockFetchCall(200, STARTUP_JS_RESPONSE_TWO),
|
||||
query: STARTUP_JS_QUERY_TWO,
|
||||
variables: { id: 3 },
|
||||
},
|
||||
],
|
||||
};
|
||||
setupLink();
|
||||
link.request(mockOperation({ operationName: OPERATION_NAME_TWO })).subscribe(result => {
|
||||
expect(result).toEqual(STARTUP_JS_RESPONSE_TWO);
|
||||
expect(startupLink.startupCalls.size).toBe(1);
|
||||
link.request(mockOperation({ operationName: OPERATION_NAME })).subscribe(result2 => {
|
||||
expect(result2).toEqual(STARTUP_JS_RESPONSE);
|
||||
expect(startupLink.startupCalls.size).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -3,11 +3,30 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Types::Ci::DetailedStatusType do
|
||||
include GraphqlHelpers
|
||||
|
||||
specify { expect(described_class.graphql_name).to eq('DetailedStatus') }
|
||||
|
||||
it "has all fields" do
|
||||
it 'has all fields' do
|
||||
expect(described_class).to have_graphql_fields(:group, :icon, :favicon,
|
||||
:details_path, :has_details,
|
||||
:label, :text, :tooltip, :action)
|
||||
end
|
||||
|
||||
describe 'action field' do
|
||||
it 'correctly renders the field' do
|
||||
stage = create(:ci_stage_entity, status: :skipped)
|
||||
status = stage.detailed_status(stage.pipeline.user)
|
||||
|
||||
expected_status = {
|
||||
button_title: status.action_button_title,
|
||||
icon: status.action_icon,
|
||||
method: status.action_method,
|
||||
path: status.action_path,
|
||||
title: status.action_title
|
||||
}
|
||||
|
||||
expect(resolve_field('action', status)).to eq(expected_status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -50,6 +50,24 @@ RSpec.describe SortingHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#search_sort_direction_button' do
|
||||
before do
|
||||
set_sorting_url 'test_label'
|
||||
end
|
||||
|
||||
it 'keeps label filter param' do
|
||||
expect(search_sort_direction_button('created_asc')).to include('label_name=test_label')
|
||||
end
|
||||
|
||||
it 'returns icon with sort-lowest when sort is asc' do
|
||||
expect(search_sort_direction_button('created_asc')).to include('sort-lowest')
|
||||
end
|
||||
|
||||
it 'returns icon with sort-highest when sort is desc' do
|
||||
expect(search_sort_direction_button('created_desc')).to include('sort-highest')
|
||||
end
|
||||
end
|
||||
|
||||
def stub_controller_path(value)
|
||||
allow(helper.controller).to receive(:controller_path).and_return(value)
|
||||
end
|
||||
|
|
|
@ -183,6 +183,25 @@ RSpec.describe Emails::ServiceDesk do
|
|||
|
||||
it_behaves_like 'handle template content', 'new_note'
|
||||
end
|
||||
|
||||
context 'with upload link in the note' do
|
||||
let_it_be(:upload_path) { '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' }
|
||||
let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project, note: "a new comment with [file](#{upload_path})") }
|
||||
|
||||
let(:template_content) { 'some text %{ NOTE_TEXT }' }
|
||||
let(:expected_body) { %Q(some text a new comment with <a href="#{project.web_url}#{upload_path}" data-link="true" class="gfm">file</a>) }
|
||||
|
||||
it_behaves_like 'handle template content', 'new_note'
|
||||
end
|
||||
|
||||
context 'with all-user reference in a an external author comment' do
|
||||
let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project, note: "Hey @all, just a ping", author: User.support_bot) }
|
||||
|
||||
let(:template_content) { 'some text %{ NOTE_TEXT }' }
|
||||
let(:expected_body) { 'Hey , just a ping' }
|
||||
|
||||
it_behaves_like 'handle template content', 'new_note'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -112,6 +112,16 @@ module GraphqlHelpers
|
|||
end
|
||||
end
|
||||
|
||||
def resolve_field(name, object, args = {})
|
||||
context = double("Context",
|
||||
schema: GitlabSchema,
|
||||
query: GraphQL::Query.new(GitlabSchema),
|
||||
parent: nil)
|
||||
field = described_class.fields[name]
|
||||
instance = described_class.authorized_new(object, context)
|
||||
field.resolve_field(instance, {}, context)
|
||||
end
|
||||
|
||||
# Recursively convert a Hash with Ruby-style keys to GraphQL fieldname-style keys
|
||||
#
|
||||
# prepare_input_for_mutation({ 'my_key' => 1 })
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
RSpec.shared_examples 'search results sorted' do
|
||||
context 'sort: newest' do
|
||||
let(:sort) { 'newest' }
|
||||
let(:sort) { 'created_desc' }
|
||||
|
||||
it 'sorts results by created_at' do
|
||||
expect(results.objects(scope).map(&:id)).to eq([new_result.id, old_result.id, very_old_result.id])
|
||||
|
@ -10,7 +10,7 @@ RSpec.shared_examples 'search results sorted' do
|
|||
end
|
||||
|
||||
context 'sort: oldest' do
|
||||
let(:sort) { 'oldest' }
|
||||
let(:sort) { 'created_asc' }
|
||||
|
||||
it 'sorts results by created_at' do
|
||||
expect(results.objects(scope).map(&:id)).to eq([very_old_result.id, old_result.id, new_result.id])
|
||||
|
|
Loading…
Reference in New Issue