Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
afed1a4be1
commit
c6283014fe
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 387 KiB After Width: | Height: | Size: 387 KiB |
|
|
@ -18,6 +18,7 @@ import TreeList from './tree_list.vue';
|
|||
|
||||
import HiddenFilesWarning from './hidden_files_warning.vue';
|
||||
import MergeConflictWarning from './merge_conflict_warning.vue';
|
||||
import CollapsedFilesWarning from './collapsed_files_warning.vue';
|
||||
|
||||
import {
|
||||
TREE_LIST_WIDTH_STORAGE_KEY,
|
||||
|
|
@ -27,6 +28,9 @@ import {
|
|||
TREE_HIDE_STATS_WIDTH,
|
||||
MR_TREE_SHOW_KEY,
|
||||
CENTERED_LIMITED_CONTAINER_CLASSES,
|
||||
ALERT_OVERFLOW_HIDDEN,
|
||||
ALERT_MERGE_CONFLICT,
|
||||
ALERT_COLLAPSED_FILES,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
|
|
@ -37,6 +41,7 @@ export default {
|
|||
NoChanges,
|
||||
HiddenFilesWarning,
|
||||
MergeConflictWarning,
|
||||
CollapsedFilesWarning,
|
||||
CommitWidget,
|
||||
TreeList,
|
||||
GlLoadingIcon,
|
||||
|
|
@ -45,6 +50,11 @@ export default {
|
|||
GlSprintf,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
alerts: {
|
||||
ALERT_OVERFLOW_HIDDEN,
|
||||
ALERT_MERGE_CONFLICT,
|
||||
ALERT_COLLAPSED_FILES,
|
||||
},
|
||||
props: {
|
||||
endpoint: {
|
||||
type: String,
|
||||
|
|
@ -114,6 +124,7 @@ export default {
|
|||
return {
|
||||
treeWidth,
|
||||
diffFilesLength: 0,
|
||||
collapsedWarningDismissed: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -142,7 +153,7 @@ export default {
|
|||
'canMerge',
|
||||
'hasConflicts',
|
||||
]),
|
||||
...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
|
||||
...mapGetters('diffs', ['hasCollapsedFile', 'isParallelView', 'currentDiffIndex']),
|
||||
...mapGetters(['isNotesFetched', 'getNoteableData']),
|
||||
diffs() {
|
||||
if (!this.viewDiffsFileByFile) {
|
||||
|
|
@ -188,6 +199,23 @@ export default {
|
|||
|
||||
return currentFileNumber < diffFiles.length ? currentFileNumber + 1 : null;
|
||||
},
|
||||
visibleWarning() {
|
||||
let visible = false;
|
||||
|
||||
if (this.renderOverflowWarning) {
|
||||
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
|
||||
) {
|
||||
visible = this.$options.alerts.ALERT_COLLAPSED_FILES;
|
||||
}
|
||||
|
||||
return visible;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
commit(newCommit, oldCommit) {
|
||||
|
|
@ -401,6 +429,9 @@ export default {
|
|||
this.toggleShowTreeList(false);
|
||||
}
|
||||
},
|
||||
dismissCollapsedWarning() {
|
||||
this.collapsedWarningDismissed = true;
|
||||
},
|
||||
},
|
||||
minTreeWidth: MIN_TREE_WIDTH,
|
||||
maxTreeWidth: MAX_TREE_WIDTH,
|
||||
|
|
@ -418,18 +449,23 @@ export default {
|
|||
/>
|
||||
|
||||
<hidden-files-warning
|
||||
v-if="renderOverflowWarning"
|
||||
v-if="visibleWarning == $options.alerts.ALERT_OVERFLOW_HIDDEN"
|
||||
:visible="numVisibleFiles"
|
||||
:total="numTotalFiles"
|
||||
:plain-diff-path="plainDiffPath"
|
||||
:email-patch-path="emailPatchPath"
|
||||
/>
|
||||
<merge-conflict-warning
|
||||
v-if="isDiffHead && hasConflicts"
|
||||
v-if="visibleWarning == $options.alerts.ALERT_MERGE_CONFLICT"
|
||||
:limited="isLimitedContainer"
|
||||
:resolution-path="conflictResolutionPath"
|
||||
:mergeable="canMerge"
|
||||
/>
|
||||
<collapsed-files-warning
|
||||
v-if="visibleWarning == $options.alerts.ALERT_COLLAPSED_FILES"
|
||||
:limited="isLimitedContainer"
|
||||
@dismiss="dismissCollapsedWarning"
|
||||
/>
|
||||
|
||||
<div
|
||||
:data-can-create-note="getNoteableData.current_user.can_create_note"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
import { GlAlert, GlButton } from '@gitlab/ui';
|
||||
|
||||
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
limited: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
dismissed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDismissed: this.dismissed,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
containerClasses() {
|
||||
return {
|
||||
[CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('diffs', ['expandAllFiles']),
|
||||
dismiss() {
|
||||
this.isDismissed = true;
|
||||
this.$emit('dismiss');
|
||||
},
|
||||
expand() {
|
||||
this.expandAllFiles();
|
||||
this.dismiss();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!isDismissed" data-testid="root" :class="containerClasses">
|
||||
<gl-alert
|
||||
:dismissible="true"
|
||||
:title="__('Some changes are not shown')"
|
||||
variant="warning"
|
||||
class="gl-mb-5"
|
||||
@dismiss="dismiss"
|
||||
>
|
||||
<p class="gl-mb-0">
|
||||
{{ __('For a faster browsing experience, some files are collapsed by default.') }}
|
||||
</p>
|
||||
<template #actions>
|
||||
<gl-button category="secondary" variant="warning" class="gl-alert-action" @click="expand">
|
||||
{{ __('Expand all files') }}
|
||||
</gl-button>
|
||||
</template>
|
||||
</gl-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -49,8 +49,12 @@ export default {
|
|||
...mapState({
|
||||
projectPath: state => state.diffs.projectPath,
|
||||
}),
|
||||
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
|
||||
...mapGetters('diffs', ['getCommentFormForDiffFile']),
|
||||
...mapGetters('diffs', [
|
||||
'isInlineView',
|
||||
'isParallelView',
|
||||
'getCommentFormForDiffFile',
|
||||
'diffLines',
|
||||
]),
|
||||
...mapGetters(['getNoteableData', 'noteableType', 'getUserData']),
|
||||
diffMode() {
|
||||
return getDiffMode(this.diffFile);
|
||||
|
|
@ -115,17 +119,15 @@ export default {
|
|||
<inline-diff-view
|
||||
v-if="isInlineView"
|
||||
:diff-file="diffFile"
|
||||
:diff-lines="
|
||||
glFeatures.unifiedDiffLines
|
||||
? diffFile.parallel_diff_lines
|
||||
: diffFile.highlighted_diff_lines || []
|
||||
"
|
||||
:diff-lines="diffFile.highlighted_diff_lines"
|
||||
:help-page-path="helpPagePath"
|
||||
/>
|
||||
<parallel-diff-view
|
||||
v-else-if="isParallelView"
|
||||
:diff-file="diffFile"
|
||||
:diff-lines="diffFile.parallel_diff_lines || []"
|
||||
:diff-lines="
|
||||
glFeatures.unifiedDiffLines ? diffLines(diffFile) : diffFile.parallel_diff_lines || []
|
||||
"
|
||||
:help-page-path="helpPagePath"
|
||||
/>
|
||||
<gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" />
|
||||
|
|
|
|||
|
|
@ -67,9 +67,7 @@ export default {
|
|||
computed: {
|
||||
...mapState({
|
||||
diffViewType(state) {
|
||||
return this.glFeatures.unifiedDiffLines
|
||||
? PARALLEL_DIFF_VIEW_TYPE
|
||||
: state.diffs.diffViewType;
|
||||
return this.glFeatures.unifiedDiffLines ? INLINE_DIFF_VIEW_TYPE : state.diffs.diffViewType;
|
||||
},
|
||||
diffFiles: state => state.diffs.diffFiles,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -64,99 +64,37 @@ export default {
|
|||
<col />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<template v-if="glFeatures.unifiedDiffLines">
|
||||
<template v-for="({ left, right }, index) in diffLines">
|
||||
<inline-diff-expansion-row
|
||||
:key="`expand-${index}`"
|
||||
:file-hash="diffFile.file_hash"
|
||||
:context-lines-path="diffFile.context_lines_path"
|
||||
:line="left || right"
|
||||
:is-top="index === 0"
|
||||
:is-bottom="index + 1 === diffLinesLength"
|
||||
/>
|
||||
<template v-if="left">
|
||||
<inline-diff-table-row
|
||||
:key="`${left.line_code || index}`"
|
||||
:file-hash="diffFile.file_hash"
|
||||
:file-path="diffFile.file_path"
|
||||
:line="left"
|
||||
:is-bottom="index + 1 === diffLinesLength"
|
||||
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
|
||||
/>
|
||||
<inline-diff-comment-row
|
||||
:key="`icr-${left.line_code || index}`"
|
||||
:diff-file-hash="diffFile.file_hash"
|
||||
:line="left"
|
||||
:help-page-path="helpPagePath"
|
||||
:has-draft="shouldRenderDraftRow(diffFile.file_hash, left) || false"
|
||||
/>
|
||||
<inline-draft-comment-row
|
||||
v-if="shouldRenderDraftRow(diffFile.file_hash, left)"
|
||||
:key="`draft_${index}`"
|
||||
:draft="draftForLine(diffFile.file_hash, left)"
|
||||
:diff-file="diffFile"
|
||||
:line="left"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="right && right.type === 'new'">
|
||||
<inline-diff-table-row
|
||||
:key="`new-${right.line_code || index}`"
|
||||
:file-hash="diffFile.file_hash"
|
||||
:file-path="diffFile.file_path"
|
||||
:line="right"
|
||||
:is-bottom="index + 1 === diffLinesLength"
|
||||
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
|
||||
/>
|
||||
<inline-diff-comment-row
|
||||
:key="`new-icr-${right.line_code || index}`"
|
||||
:diff-file-hash="diffFile.file_hash"
|
||||
:line="right"
|
||||
:help-page-path="helpPagePath"
|
||||
:has-draft="shouldRenderDraftRow(diffFile.file_hash, right) || false"
|
||||
/>
|
||||
<inline-draft-comment-row
|
||||
v-if="shouldRenderDraftRow(diffFile.file_hash, right)"
|
||||
:key="`new-draft_${index}`"
|
||||
:draft="draftForLine(diffFile.file_hash, right)"
|
||||
:diff-file="diffFile"
|
||||
:line="right"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="(line, index) in diffLines">
|
||||
<inline-diff-expansion-row
|
||||
:key="`expand-${index}`"
|
||||
:file-hash="diffFile.file_hash"
|
||||
:context-lines-path="diffFile.context_lines_path"
|
||||
:line="line"
|
||||
:is-top="index === 0"
|
||||
:is-bottom="index + 1 === diffLinesLength"
|
||||
/>
|
||||
<inline-diff-table-row
|
||||
:key="`${line.line_code || index}`"
|
||||
:file-hash="diffFile.file_hash"
|
||||
:file-path="diffFile.file_path"
|
||||
:line="line"
|
||||
:is-bottom="index + 1 === diffLinesLength"
|
||||
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
|
||||
/>
|
||||
<inline-diff-comment-row
|
||||
:key="`icr-${line.line_code || index}`"
|
||||
:diff-file-hash="diffFile.file_hash"
|
||||
:line="line"
|
||||
:help-page-path="helpPagePath"
|
||||
:has-draft="shouldRenderDraftRow(diffFile.file_hash, line) || false"
|
||||
/>
|
||||
<inline-draft-comment-row
|
||||
v-if="shouldRenderDraftRow(diffFile.file_hash, line)"
|
||||
:key="`draft_${index}`"
|
||||
:draft="draftForLine(diffFile.file_hash, line)"
|
||||
:diff-file="diffFile"
|
||||
:line="line"
|
||||
/>
|
||||
</template>
|
||||
<template v-for="(line, index) in diffLines">
|
||||
<inline-diff-expansion-row
|
||||
:key="`expand-${index}`"
|
||||
:file-hash="diffFile.file_hash"
|
||||
:context-lines-path="diffFile.context_lines_path"
|
||||
:line="line"
|
||||
:is-top="index === 0"
|
||||
:is-bottom="index + 1 === diffLinesLength"
|
||||
/>
|
||||
<inline-diff-table-row
|
||||
:key="`${line.line_code || index}`"
|
||||
:file-hash="diffFile.file_hash"
|
||||
:file-path="diffFile.file_path"
|
||||
:line="line"
|
||||
:is-bottom="index + 1 === diffLinesLength"
|
||||
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
|
||||
/>
|
||||
<inline-diff-comment-row
|
||||
:key="`icr-${line.line_code || index}`"
|
||||
:diff-file-hash="diffFile.file_hash"
|
||||
:line="line"
|
||||
:help-page-path="helpPagePath"
|
||||
:has-draft="shouldRenderDraftRow(diffFile.file_hash, line) || false"
|
||||
/>
|
||||
<inline-draft-comment-row
|
||||
v-if="shouldRenderDraftRow(diffFile.file_hash, line)"
|
||||
:key="`draft_${index}`"
|
||||
:draft="draftForLine(diffFile.file_hash, line)"
|
||||
:diff-file="diffFile"
|
||||
:line="line"
|
||||
/>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -68,6 +68,11 @@ export const DIFFS_PER_PAGE = 20;
|
|||
export const DIFF_COMPARE_BASE_VERSION_INDEX = -1;
|
||||
export const DIFF_COMPARE_HEAD_VERSION_INDEX = -2;
|
||||
|
||||
// Diff View Alerts
|
||||
export const ALERT_OVERFLOW_HIDDEN = 'overflow';
|
||||
export const ALERT_MERGE_CONFLICT = 'merge-conflict';
|
||||
export const ALERT_COLLAPSED_FILES = 'collapsed';
|
||||
|
||||
// State machine states
|
||||
export const STATE_IDLING = 'idle';
|
||||
export const STATE_LOADING = 'loading';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { __, n__ } from '~/locale';
|
||||
import { parallelizeDiffLines } from './utils';
|
||||
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
|
||||
|
||||
export * from './getters_versions_dropdowns';
|
||||
|
|
@ -129,3 +130,11 @@ export const fileLineCoverage = state => (file, line) => {
|
|||
*/
|
||||
export const currentDiffIndex = state =>
|
||||
Math.max(0, state.diffFiles.findIndex(diff => diff.file_hash === state.currentDiffFileId));
|
||||
|
||||
export const diffLines = state => file => {
|
||||
if (state.diffViewType === INLINE_DIFF_VIEW_TYPE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parallelizeDiffLines(file.highlighted_diff_lines || []);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { PARALLEL_DIFF_VIEW_TYPE } from '../constants';
|
||||
import { INLINE_DIFF_VIEW_TYPE } from '../constants';
|
||||
import {
|
||||
findDiffFile,
|
||||
addLineReferences,
|
||||
|
|
@ -152,7 +152,7 @@ export default {
|
|||
inlineLines: diffFile.highlighted_diff_lines,
|
||||
parallelLines: diffFile.parallel_diff_lines,
|
||||
diffViewType: window.gon?.features?.unifiedDiffLines
|
||||
? PARALLEL_DIFF_VIEW_TYPE
|
||||
? INLINE_DIFF_VIEW_TYPE
|
||||
: state.diffViewType,
|
||||
contextLines: lines,
|
||||
bottom,
|
||||
|
|
|
|||
|
|
@ -350,15 +350,8 @@ function mergeTwoFiles(target, source) {
|
|||
}
|
||||
|
||||
function ensureBasicDiffFileLines(file) {
|
||||
if (window.gon?.features?.unifiedDiffLines) {
|
||||
return Object.assign(file, {
|
||||
highlighted_diff_lines: [],
|
||||
parallel_diff_lines: parallelizeDiffLines(file.highlighted_diff_lines || []),
|
||||
});
|
||||
}
|
||||
|
||||
const missingInline = !file.highlighted_diff_lines;
|
||||
const missingParallel = !file.parallel_diff_lines;
|
||||
const missingParallel = !file.parallel_diff_lines || window.gon?.features?.unifiedDiffLines;
|
||||
|
||||
Object.assign(file, {
|
||||
highlighted_diff_lines: missingInline ? [] : file.highlighted_diff_lines,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import UsersSelect from './users_select';
|
|||
export default class IssuableContext {
|
||||
constructor(currentUser) {
|
||||
this.userSelect = new UsersSelect(currentUser);
|
||||
this.reviewersSelect = new UsersSelect(currentUser, '.js-reviewer-search');
|
||||
|
||||
import(/* webpackChunkName: 'select2' */ 'select2/select2')
|
||||
.then(() => {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,17 @@ export default class SidebarMediator {
|
|||
return this.service.update(field, data);
|
||||
}
|
||||
|
||||
saveReviewers(field) {
|
||||
const selected = this.store.reviewers.map(u => u.id);
|
||||
|
||||
// If there are no ids, that means we have to unassign (which is id = 0)
|
||||
// And it only accepts an array, hence [0]
|
||||
const reviewers = selected.length === 0 ? [0] : selected;
|
||||
const data = { reviewer_ids: reviewers };
|
||||
|
||||
return this.service.update(field, data);
|
||||
}
|
||||
|
||||
setMoveToProjectId(projectId) {
|
||||
this.store.setMoveToProjectId(projectId);
|
||||
}
|
||||
|
|
@ -55,6 +66,7 @@ export default class SidebarMediator {
|
|||
|
||||
processFetchedData(data) {
|
||||
this.store.setAssigneeData(data);
|
||||
this.store.setReviewerData(data);
|
||||
this.store.setTimeTrackingData(data);
|
||||
this.store.setParticipantsData(data);
|
||||
this.store.setSubscriptionsData(data);
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ export default class SidebarStore {
|
|||
this.humanTimeSpent = '';
|
||||
this.timeTrackingLimitToHours = timeTrackingLimitToHours;
|
||||
this.assignees = [];
|
||||
this.reviewers = [];
|
||||
this.isFetching = {
|
||||
assignees: true,
|
||||
reviewers: true,
|
||||
participants: true,
|
||||
subscriptions: true,
|
||||
};
|
||||
|
|
@ -42,6 +44,13 @@ export default class SidebarStore {
|
|||
}
|
||||
}
|
||||
|
||||
setReviewerData(data) {
|
||||
this.isFetching.reviewers = false;
|
||||
if (data.reviewers) {
|
||||
this.reviewers = data.reviewers;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeTrackingData(data) {
|
||||
this.timeEstimate = data.time_estimate;
|
||||
this.totalTimeSpent = data.total_time_spent;
|
||||
|
|
@ -75,20 +84,40 @@ export default class SidebarStore {
|
|||
}
|
||||
}
|
||||
|
||||
addReviewer(reviewer) {
|
||||
if (!this.findReviewer(reviewer)) {
|
||||
this.reviewers.push(reviewer);
|
||||
}
|
||||
}
|
||||
|
||||
findAssignee(findAssignee) {
|
||||
return this.assignees.find(assignee => assignee.id === findAssignee.id);
|
||||
}
|
||||
|
||||
findReviewer(findReviewer) {
|
||||
return this.reviewers.find(reviewer => reviewer.id === findReviewer.id);
|
||||
}
|
||||
|
||||
removeAssignee(removeAssignee) {
|
||||
if (removeAssignee) {
|
||||
this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
|
||||
}
|
||||
}
|
||||
|
||||
removeReviewer(removeReviewer) {
|
||||
if (removeReviewer) {
|
||||
this.reviewers = this.reviewers.filter(reviewer => reviewer.id !== removeReviewer.id);
|
||||
}
|
||||
}
|
||||
|
||||
removeAllAssignees() {
|
||||
this.assignees = [];
|
||||
}
|
||||
|
||||
removeAllReviewers() {
|
||||
this.reviewers = [];
|
||||
}
|
||||
|
||||
setAssigneesFromRealtime(data) {
|
||||
this.assignees = data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
|
|||
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
|
||||
|
||||
function UsersSelect(currentUser, els, options = {}) {
|
||||
const elsClassName = els?.toString().match('.(.+$)')[1];
|
||||
const $els = $(els || '.js-user-search');
|
||||
this.users = this.users.bind(this);
|
||||
this.user = this.user.bind(this);
|
||||
|
|
@ -127,9 +128,16 @@ function UsersSelect(currentUser, els, options = {}) {
|
|||
.find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`);
|
||||
|
||||
firstSelected.remove();
|
||||
emitSidebarEvent('sidebar.removeAssignee', {
|
||||
id: firstSelectedId,
|
||||
});
|
||||
|
||||
if ($dropdown.hasClass(elsClassName)) {
|
||||
emitSidebarEvent('sidebar.removeReviewer', {
|
||||
id: firstSelectedId,
|
||||
});
|
||||
} else {
|
||||
emitSidebarEvent('sidebar.removeAssignee', {
|
||||
id: firstSelectedId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -392,7 +400,11 @@ function UsersSelect(currentUser, els, options = {}) {
|
|||
defaultLabel,
|
||||
hidden() {
|
||||
if ($dropdown.hasClass('js-multiselect')) {
|
||||
emitSidebarEvent('sidebar.saveAssignees');
|
||||
if ($dropdown.hasClass(elsClassName)) {
|
||||
emitSidebarEvent('sidebar.saveReviewers');
|
||||
} else {
|
||||
emitSidebarEvent('sidebar.saveAssignees');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$dropdown.data('alwaysShowSelectbox')) {
|
||||
|
|
@ -428,10 +440,18 @@ function UsersSelect(currentUser, els, options = {}) {
|
|||
previouslySelected.each((index, element) => {
|
||||
element.remove();
|
||||
});
|
||||
emitSidebarEvent('sidebar.removeAllAssignees');
|
||||
if ($dropdown.hasClass(elsClassName)) {
|
||||
emitSidebarEvent('sidebar.removeAllReviewers');
|
||||
} else {
|
||||
emitSidebarEvent('sidebar.removeAllAssignees');
|
||||
}
|
||||
} else if (isActive) {
|
||||
// user selected
|
||||
emitSidebarEvent('sidebar.addAssignee', user);
|
||||
if ($dropdown.hasClass(elsClassName)) {
|
||||
emitSidebarEvent('sidebar.addReviewer', user);
|
||||
} else {
|
||||
emitSidebarEvent('sidebar.addAssignee', user);
|
||||
}
|
||||
|
||||
// Remove unassigned selection (if it was previously selected)
|
||||
const unassignedSelected = $dropdown
|
||||
|
|
@ -448,7 +468,11 @@ function UsersSelect(currentUser, els, options = {}) {
|
|||
}
|
||||
|
||||
// User unselected
|
||||
emitSidebarEvent('sidebar.removeAssignee', user);
|
||||
if ($dropdown.hasClass(elsClassName)) {
|
||||
emitSidebarEvent('sidebar.removeReviewer', user);
|
||||
} else {
|
||||
emitSidebarEvent('sidebar.removeAssignee', user);
|
||||
}
|
||||
}
|
||||
|
||||
if (getSelected().find(u => u === gon.current_user_id)) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
|
|||
import { sprintf, s__, __ } from '~/locale';
|
||||
import Project from '~/pages/projects/project';
|
||||
import SmartInterval from '~/smart_interval';
|
||||
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
|
||||
import { deprecatedCreateFlash as createFlash } from '../flash';
|
||||
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
|
||||
import Loading from './components/loading.vue';
|
||||
|
|
@ -202,7 +203,10 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
MRWidgetService.fetchInitialData()
|
||||
.then(({ data }) => this.initWidget(data))
|
||||
.then(({ data, headers }) => {
|
||||
this.startingPollInterval = Number(headers['POLL-INTERVAL']);
|
||||
this.initWidget(data);
|
||||
})
|
||||
.catch(() =>
|
||||
createFlash(__('Unable to load the merge request widget. Try reloading the page.')),
|
||||
);
|
||||
|
|
@ -292,9 +296,10 @@ export default {
|
|||
initPolling() {
|
||||
this.pollingInterval = new SmartInterval({
|
||||
callback: this.checkStatus,
|
||||
startingInterval: 10 * 1000,
|
||||
maxInterval: 240 * 1000,
|
||||
hiddenInterval: window.gon?.features?.widgetVisibilityPolling && 360 * 1000,
|
||||
startingInterval: this.startingPollInterval,
|
||||
maxInterval: this.startingPollInterval + secondsToMilliseconds(4 * 60),
|
||||
hiddenInterval:
|
||||
window.gon?.features?.widgetVisibilityPolling && secondsToMilliseconds(6 * 60),
|
||||
incrementByFactorOf: 2,
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { normalizeHeaders } from '~/lib/utils/common_utils';
|
||||
import axios from '../../lib/utils/axios_utils';
|
||||
|
||||
export default class MRWidgetService {
|
||||
|
|
@ -82,6 +83,11 @@ export default class MRWidgetService {
|
|||
return Promise.all([
|
||||
axios.get(window.gl.mrWidgetData.merge_request_cached_widget_path),
|
||||
axios.get(window.gl.mrWidgetData.merge_request_widget_path),
|
||||
]).then(axios.spread((res, cachedRes) => ({ data: Object.assign(res.data, cachedRes.data) })));
|
||||
]).then(
|
||||
axios.spread((res, cachedRes) => ({
|
||||
data: Object.assign(res.data, cachedRes.data),
|
||||
headers: normalizeHeaders(res.headers),
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -386,6 +386,12 @@ module IssuablesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def reviewer_sidebar_data(reviewer, merge_request: nil)
|
||||
{ avatar_url: reviewer.avatar_url, name: reviewer.name, username: reviewer.username }.tap do |data|
|
||||
data[:can_merge] = merge_request.can_be_merged_by?(reviewer) if merge_request
|
||||
end
|
||||
end
|
||||
|
||||
def issuable_squash_option?(issuable, project)
|
||||
if issuable.persisted?
|
||||
issuable.squash
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
event :pending do
|
||||
transition all => :pending
|
||||
end
|
||||
|
||||
event :manual do
|
||||
transition all => :manual
|
||||
end
|
||||
|
|
|
|||
|
|
@ -237,7 +237,6 @@ module Ci
|
|||
end
|
||||
|
||||
after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
|
||||
next unless pipeline.bridge_triggered?
|
||||
next unless pipeline.bridge_waiting?
|
||||
|
||||
pipeline.run_after_commit do
|
||||
|
|
@ -1072,6 +1071,18 @@ module Ci
|
|||
.base_and_ancestors
|
||||
end
|
||||
|
||||
# We need `base_and_ancestors` in a specific order to "break" when needed.
|
||||
# If we use `find_each`, then the order is broken.
|
||||
# rubocop:disable Rails/FindEach
|
||||
def reset_ancestor_bridges!
|
||||
base_and_ancestors.includes(:source_bridge).each do |pipeline|
|
||||
break unless pipeline.bridge_waiting?
|
||||
|
||||
pipeline.source_bridge.pending!
|
||||
end
|
||||
end
|
||||
# rubocop:enable Rails/FindEach
|
||||
|
||||
private
|
||||
|
||||
def add_message(severity, content)
|
||||
|
|
|
|||
|
|
@ -19,3 +19,5 @@ class ResourceStateEvent < ResourceEvent
|
|||
issue || merge_request
|
||||
end
|
||||
end
|
||||
|
||||
ResourceStateEvent.prepend_if_ee('EE::ResourceStateEvent')
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ module Ci
|
|||
|
||||
reprocess!(build).tap do |new_build|
|
||||
mark_subsequent_stages_as_processable(build)
|
||||
build.pipeline.reset_ancestor_bridges!
|
||||
|
||||
Gitlab::OptimisticLocking.retry_lock(new_build, &:enqueue)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ module Ci
|
|||
retry_optimistic_lock(skipped) { |build| build.process }
|
||||
end
|
||||
|
||||
pipeline.reset_ancestor_bridges!
|
||||
|
||||
MergeRequests::AddTodoWhenBuildFailsService
|
||||
.new(project, current_user)
|
||||
.close_all(pipeline)
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ module ResourceEvents
|
|||
|
||||
ResourceStateEvent.create(
|
||||
user: user,
|
||||
issue: issue,
|
||||
merge_request: merge_request,
|
||||
resource.class.underscore => resource,
|
||||
source_commit: commit_id_of(mentionable_source),
|
||||
source_merge_request_id: merge_request_id_of(mentionable_source),
|
||||
state: ResourceStateEvent.states[state],
|
||||
close_after_error_tracking_resolve: close_after_error_tracking_resolve,
|
||||
close_auto_resolve_prometheus_alert: close_auto_resolve_prometheus_alert,
|
||||
created_at: Time.zone.now)
|
||||
created_at: Time.zone.now
|
||||
)
|
||||
|
||||
resource.expire_note_etag_cache
|
||||
end
|
||||
|
|
@ -56,17 +56,5 @@ module ResourceEvents
|
|||
|
||||
mentionable_source.id
|
||||
end
|
||||
|
||||
def issue
|
||||
return unless resource.is_a?(Issue)
|
||||
|
||||
resource
|
||||
end
|
||||
|
||||
def merge_request
|
||||
return unless resource.is_a?(MergeRequest)
|
||||
|
||||
resource
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
.loading.hide
|
||||
.spinner.spinner-md
|
||||
|
||||
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, source_branch: @merge_request.source_branch
|
||||
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch
|
||||
|
||||
- if @merge_request.can_be_reverted?(current_user)
|
||||
= render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
|
||||
- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
|
||||
- add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras"
|
||||
- reviewers = local_assigns.fetch(:reviewers, nil)
|
||||
|
||||
- if Feature.enabled?(:vue_issuable_sidebar, @project.group)
|
||||
%aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in,
|
||||
|
|
@ -28,6 +29,10 @@
|
|||
.block.assignee.qa-assignee-block
|
||||
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees
|
||||
|
||||
- if reviewers
|
||||
.block.reviewer.qa-reviewer-block
|
||||
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers
|
||||
|
||||
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar
|
||||
|
||||
- if issuable_sidebar[:supports_milestone]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
- issuable_type = issuable_sidebar[:type]
|
||||
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
|
||||
|
||||
#js-vue-sidebar-reviewers{ data: { field: issuable_type, signed_in: signed_in } }
|
||||
.title.hide-collapsed
|
||||
= _('Reviewer')
|
||||
= loading_icon(css_class: 'gl-vertical-align-text-bottom')
|
||||
|
||||
.selectbox.hide-collapsed
|
||||
- if reviewers.none?
|
||||
= hidden_field_tag "#{issuable_type}[reviewer_ids][]", 0, id: nil
|
||||
- else
|
||||
- reviewers.each do |reviewer|
|
||||
= hidden_field_tag "#{issuable_type}[reviewer_ids][]", reviewer.id, id: nil, data: reviewer_sidebar_data(reviewer, merge_request: @merge_request)
|
||||
|
||||
- options = { toggle_class: 'js-reviewer-search js-author-search',
|
||||
title: _('Request review from'),
|
||||
filter: true,
|
||||
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author',
|
||||
placeholder: _('Search users'),
|
||||
data: { first_user: issuable_sidebar.dig(:current_user, :username),
|
||||
current_user: true,
|
||||
iid: issuable_sidebar[:iid],
|
||||
issuable_type: issuable_type,
|
||||
project_id: issuable_sidebar[:project_id],
|
||||
author_id: issuable_sidebar[:author_id],
|
||||
field_name: "#{issuable_type}[reviewer_ids][]",
|
||||
issue_update: issuable_sidebar[:issuable_json_path],
|
||||
ability_name: issuable_type,
|
||||
null_user: true,
|
||||
display: 'static' } }
|
||||
|
||||
- dropdown_options = reviewers_dropdown_options(issuable_type)
|
||||
- title = dropdown_options[:title]
|
||||
- options[:toggle_class] += ' js-multiselect js-save-user-data'
|
||||
- data = { field_name: "#{issuable_type}[reviewer_ids][]" }
|
||||
- data[:multi_select] = true
|
||||
- data['dropdown-title'] = title
|
||||
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
|
||||
- data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
|
||||
- options[:data].merge!(data)
|
||||
|
||||
- if experiment_enabled?(:invite_members_version_a) && can_import_members?
|
||||
- options[:dropdown_class] += ' dropdown-extended-height'
|
||||
- options[:footer_content] = true
|
||||
- options[:wrapper_class] = 'js-sidebar-reviewer-dropdown'
|
||||
|
||||
= dropdown_tag(title, options: options) do
|
||||
%ul.dropdown-footer-list
|
||||
%li
|
||||
= link_to _('Invite Members'),
|
||||
project_project_members_path(@project),
|
||||
title: _('Invite Members'),
|
||||
data: { 'is-link': true, 'track-event': 'click_invite_members', 'track-label': 'edit_reviewer' }
|
||||
- else
|
||||
= dropdown_tag(title, options: options)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
- if Feature.enabled?(:snippets_edit_vue)
|
||||
- if Feature.enabled?(:snippets_edit_vue, default_enabled: true)
|
||||
- available_visibility_levels = available_visibility_levels(@snippet)
|
||||
#js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access"), 'visibility_levels': available_visibility_levels, 'selected_level': snippets_selected_visibility_level(available_visibility_levels, @snippet.visibility_level), 'multiple_levels_restricted': multiple_visibility_levels_restricted? } }
|
||||
- else
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Refactored snippets edit form to Vue
|
||||
merge_request: 42412
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix upstream pipeline status when strategy dependent
|
||||
merge_request: 41930
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix GitLab file icon in Firefox
|
||||
merge_request: 40786
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Display Contributor and Author badges on notes
|
||||
merge_request: 40198
|
||||
author: Mycroft Kang @TaehyeokKang
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add a warning when any diff files are collapsed
|
||||
merge_request: 40752
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: DAST Site validation - Model Layer
|
||||
merge_request: 41639
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Increase widget polling for closed and merged merge requests
|
||||
merge_request: 42458
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: snippets_edit_vue
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25667
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/207239
|
||||
group: group::editor
|
||||
type: development
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
- title: Coverage-guided fuzz testing for Go and C/C++ applications
|
||||
body: You can now run coverage-guided fuzz tests against your Go and C/C++ apps. This is a great way to start finding security issues and bugs that other security scanners and traditional QA may miss. Coverage-guided fuzz testing uses contextual information about your app to randomly generate inputs and find crashes or other faults that you can then fix before they affect users in production.
|
||||
stage: Secure
|
||||
self-managed: true
|
||||
gitlab-com: true
|
||||
packages: [Ultimate]
|
||||
url: https://www.youtube.com/watch?v=3wdWMDRLdp4
|
||||
image_url: http://i3.ytimg.com/vi/3wdWMDRLdp4/hqdefault.jpg
|
||||
published_at: 2020-08-22
|
||||
release: 13.3
|
||||
- title: Create a matrix of jobs using a simple syntax
|
||||
body: GitLab’s child/parent pipelines let you write your own code to generate an entire pipeline YAML. This is a powerful way to generate custom behaviors, including generating jobs at runtime. This might not be needed for simpler scenarios where you just want to create multiple similar jobs for a defined set of cases. In this release you can find a new matrix keyword that works along with parallel to handle the creation of multiple jobs for you, each with different variables.
|
||||
stage: Verify
|
||||
self-managed: true
|
||||
gitlab-com: true
|
||||
packages: [All]
|
||||
url: https://docs.gitlab.com/ee/ci/yaml/#parallel-matrix-jobs
|
||||
image_url: https://about.gitlab.com/images/13_3/cartesian-matrix.png
|
||||
published_at: 2020-08-22
|
||||
release: 13.3
|
||||
- title: On-demand DAST scans
|
||||
body: Dynamic Application Security Testing at GitLab has always been focused on integrating DAST into the DevOps pipeline and enabling developers to scan their review app, running website, or API for vulnerabilities as early as possible. However, there are times when it is necessary to run a DAST scan against an already deployed application when no code changes have been made and no Merge Request has been created. These scans could be needed for audit or compliance reasons, to debug and reproduce an issue that has been found, or to support teams who do not commit code, such as security analysts. Because of the need for DAST scans that are not triggered by a code change or MR, on-demand DAST testing is now available. You don’t need configuration files or code to start running on-demand scans. Configuration options for on-demand DAST scans are available within the GitLab UI.
|
||||
stage: Secure
|
||||
self-managed: true
|
||||
gitlab-com: true
|
||||
packages: [Ultimate]
|
||||
url: https://docs.gitlab.com/ee/user/application_security/dast/#on-demand-scans
|
||||
image_url: https://about.gitlab.com/images/13_3/dast_on_demand_v13_3.png
|
||||
published_at: 2020-08-22
|
||||
release: 13.3
|
||||
- title: SAST security analyzers available for all
|
||||
body: We want to help developers write better code and worry less about common security mistakes. Static Application Security Testing (SAST) helps prevent security vulnerabilities by allowing developers to easily identify common security issues as code is being committed and mitigate proactively. As part of our community stewardship commitment we have made all 15 of our open source based SAST analyzers available in every GitLab tier. This allows ALL GitLab users developing in any of our 18 supported languages and frameworks to leverage GitLab SAST in their projects.
|
||||
stage: Secure
|
||||
self-managed: true
|
||||
gitlab-com: true
|
||||
packages: [All]
|
||||
url: https://docs.gitlab.com/ee/user/application_security/sast/#making-sast-analyzers-available-to-all-gitlab-tiers
|
||||
image_url: https://about.gitlab.com/images/13_3/sast-gitlab-languages.png
|
||||
published_at: 2020-08-22
|
||||
release: 13.3
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateDastSiteTokens < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
unless table_exists?(:dast_site_tokens)
|
||||
with_lock_retries do
|
||||
create_table :dast_site_tokens do |t|
|
||||
t.references :project, foreign_key: { on_delete: :cascade }, null: false, index: true
|
||||
|
||||
t.timestamps_with_timezone null: false
|
||||
t.datetime_with_timezone :expired_at
|
||||
|
||||
t.text :token, null: false, unique: true
|
||||
t.text :url, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
add_text_limit :dast_site_tokens, :token, 255
|
||||
add_text_limit :dast_site_tokens, :url, 255
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
drop_table :dast_site_tokens
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateDastSiteValidations < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
unless table_exists?(:dast_site_validations)
|
||||
with_lock_retries do
|
||||
create_table :dast_site_validations do |t|
|
||||
t.references :dast_site_token, foreign_key: { on_delete: :cascade }, null: false, index: true
|
||||
|
||||
t.timestamps_with_timezone null: false
|
||||
t.datetime_with_timezone :validation_started_at
|
||||
t.datetime_with_timezone :validation_passed_at
|
||||
t.datetime_with_timezone :validation_failed_at
|
||||
t.datetime_with_timezone :validation_last_retried_at
|
||||
|
||||
t.integer :validation_strategy, null: false, limit: 2
|
||||
|
||||
t.text :url_base, null: false
|
||||
t.text :url_path, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
add_concurrent_index :dast_site_validations, :url_base
|
||||
add_text_limit :dast_site_validations, :url_base, 255
|
||||
add_text_limit :dast_site_validations, :url_path, 255
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
drop_table :dast_site_validations
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDastSiteValidationIdToDastSite < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
TABLE_NAME = :dast_sites
|
||||
RELATION_NAME = :dast_site_validations
|
||||
FK_NAME = :dast_site_validation_id
|
||||
INDEX_NAME = "index_dast_sites_on_#{FK_NAME}"
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
unless column_exists?(TABLE_NAME, FK_NAME)
|
||||
with_lock_retries do
|
||||
add_column TABLE_NAME, FK_NAME, :bigint
|
||||
end
|
||||
end
|
||||
|
||||
add_concurrent_index TABLE_NAME, FK_NAME, name: INDEX_NAME
|
||||
add_concurrent_foreign_key TABLE_NAME, RELATION_NAME, column: FK_NAME, on_delete: :nullify
|
||||
end
|
||||
|
||||
def down
|
||||
remove_foreign_key_if_exists TABLE_NAME, RELATION_NAME
|
||||
remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
|
||||
|
||||
with_lock_retries do
|
||||
remove_column TABLE_NAME, FK_NAME
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
5fba5213226186a1506f672eb3eab2d07f58b019c4ba13760663cb119f62d4e2
|
||||
|
|
@ -0,0 +1 @@
|
|||
002c92f830762d97dcbdbcf8a0287ebbb576edc27f4f76f4bb18d043e956ba7a
|
||||
|
|
@ -0,0 +1 @@
|
|||
5f932b8a3503fc275ba6d09436115999b32f6438700e3b719f53730c5527a354
|
||||
|
|
@ -11202,12 +11202,59 @@ CREATE SEQUENCE public.dast_site_profiles_id_seq
|
|||
|
||||
ALTER SEQUENCE public.dast_site_profiles_id_seq OWNED BY public.dast_site_profiles.id;
|
||||
|
||||
CREATE TABLE public.dast_site_tokens (
|
||||
id bigint NOT NULL,
|
||||
project_id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
expired_at timestamp with time zone,
|
||||
token text NOT NULL,
|
||||
url text NOT NULL,
|
||||
CONSTRAINT check_02a6bf20a7 CHECK ((char_length(token) <= 255)),
|
||||
CONSTRAINT check_69ab8622a6 CHECK ((char_length(url) <= 255))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.dast_site_tokens_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE public.dast_site_tokens_id_seq OWNED BY public.dast_site_tokens.id;
|
||||
|
||||
CREATE TABLE public.dast_site_validations (
|
||||
id bigint NOT NULL,
|
||||
dast_site_token_id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
validation_started_at timestamp with time zone,
|
||||
validation_passed_at timestamp with time zone,
|
||||
validation_failed_at timestamp with time zone,
|
||||
validation_last_retried_at timestamp with time zone,
|
||||
validation_strategy smallint NOT NULL,
|
||||
url_base text NOT NULL,
|
||||
url_path text NOT NULL,
|
||||
CONSTRAINT check_13b34efe4b CHECK ((char_length(url_path) <= 255)),
|
||||
CONSTRAINT check_cd3b538210 CHECK ((char_length(url_base) <= 255))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.dast_site_validations_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE public.dast_site_validations_id_seq OWNED BY public.dast_site_validations.id;
|
||||
|
||||
CREATE TABLE public.dast_sites (
|
||||
id bigint NOT NULL,
|
||||
project_id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
url text NOT NULL,
|
||||
dast_site_validation_id bigint,
|
||||
CONSTRAINT check_46df8b449c CHECK ((char_length(url) <= 255))
|
||||
);
|
||||
|
||||
|
|
@ -17166,6 +17213,10 @@ ALTER TABLE ONLY public.dast_scanner_profiles ALTER COLUMN id SET DEFAULT nextva
|
|||
|
||||
ALTER TABLE ONLY public.dast_site_profiles ALTER COLUMN id SET DEFAULT nextval('public.dast_site_profiles_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY public.dast_site_tokens ALTER COLUMN id SET DEFAULT nextval('public.dast_site_tokens_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY public.dast_site_validations ALTER COLUMN id SET DEFAULT nextval('public.dast_site_validations_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY public.dast_sites ALTER COLUMN id SET DEFAULT nextval('public.dast_sites_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY public.dependency_proxy_blobs ALTER COLUMN id SET DEFAULT nextval('public.dependency_proxy_blobs_id_seq'::regclass);
|
||||
|
|
@ -18195,6 +18246,12 @@ ALTER TABLE ONLY public.dast_scanner_profiles
|
|||
ALTER TABLE ONLY public.dast_site_profiles
|
||||
ADD CONSTRAINT dast_site_profiles_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.dast_site_tokens
|
||||
ADD CONSTRAINT dast_site_tokens_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.dast_site_validations
|
||||
ADD CONSTRAINT dast_site_validations_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.dast_sites
|
||||
ADD CONSTRAINT dast_sites_pkey PRIMARY KEY (id);
|
||||
|
||||
|
|
@ -19785,6 +19842,14 @@ CREATE INDEX index_dast_site_profiles_on_dast_site_id ON public.dast_site_profil
|
|||
|
||||
CREATE UNIQUE INDEX index_dast_site_profiles_on_project_id_and_name ON public.dast_site_profiles USING btree (project_id, name);
|
||||
|
||||
CREATE INDEX index_dast_site_tokens_on_project_id ON public.dast_site_tokens USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_dast_site_validations_on_dast_site_token_id ON public.dast_site_validations USING btree (dast_site_token_id);
|
||||
|
||||
CREATE INDEX index_dast_site_validations_on_url_base ON public.dast_site_validations USING btree (url_base);
|
||||
|
||||
CREATE INDEX index_dast_sites_on_dast_site_validation_id ON public.dast_sites USING btree (dast_site_validation_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_dast_sites_on_project_id_and_url ON public.dast_sites USING btree (project_id, url);
|
||||
|
||||
CREATE INDEX index_dependency_proxy_blobs_on_group_id_and_file_name ON public.dependency_proxy_blobs USING btree (group_id, file_name);
|
||||
|
|
@ -21754,6 +21819,9 @@ ALTER TABLE ONLY public.merge_requests
|
|||
ALTER TABLE ONLY public.user_interacted_projects
|
||||
ADD CONSTRAINT fk_0894651f08 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.dast_sites
|
||||
ADD CONSTRAINT fk_0a57f2271b FOREIGN KEY (dast_site_validation_id) REFERENCES public.dast_site_validations(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY public.web_hooks
|
||||
ADD CONSTRAINT fk_0c8ca6d9d1 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
@ -22537,6 +22605,9 @@ ALTER TABLE ONLY public.lfs_file_locks
|
|||
ALTER TABLE ONLY public.project_alerting_settings
|
||||
ADD CONSTRAINT fk_rails_27a84b407d FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.dast_site_validations
|
||||
ADD CONSTRAINT fk_rails_285c617324 FOREIGN KEY (dast_site_token_id) REFERENCES public.dast_site_tokens(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.resource_state_events
|
||||
ADD CONSTRAINT fk_rails_29af06892a FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
@ -23464,6 +23535,9 @@ ALTER TABLE ONLY public.merge_request_metrics
|
|||
ALTER TABLE ONLY public.draft_notes
|
||||
ADD CONSTRAINT fk_rails_e753681674 FOREIGN KEY (merge_request_id) REFERENCES public.merge_requests(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.dast_site_tokens
|
||||
ADD CONSTRAINT fk_rails_e84f721a8e FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.group_deploy_keys_groups
|
||||
ADD CONSTRAINT fk_rails_e87145115d FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
|||
|
|
@ -218,6 +218,17 @@ namespace = Namespace.find_by_full_path("")
|
|||
::Projects::TransferService.new(p, current_user).execute(namespace)
|
||||
```
|
||||
|
||||
### For Removing webhooks that is getting timeout due to large webhook logs
|
||||
|
||||
```ruby
|
||||
# ID will be the webhook_id
|
||||
WebHookLog.where(web_hook_id: ID).each_slice(ID) do |slice|
|
||||
slice.each(&:destroy)
|
||||
end
|
||||
|
||||
WebHook.find(ID).destroy
|
||||
```
|
||||
|
||||
### Bulk update service integration password for _all_ projects
|
||||
|
||||
For example, change the Jira user's password for all projects that have the Jira
|
||||
|
|
|
|||
|
|
@ -44259,59 +44259,6 @@
|
|||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "variables",
|
||||
"description": "List of supported variables",
|
||||
"args": [
|
||||
{
|
||||
"name": "after",
|
||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "before",
|
||||
"description": "Returns the elements in the list that come before the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "first",
|
||||
"description": "Returns the first _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "last",
|
||||
"description": "Returns the last _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SastCiConfigurationEntityConnection",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
|
|
@ -44379,6 +44326,59 @@
|
|||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "variables",
|
||||
"description": "List of supported variables",
|
||||
"args": [
|
||||
{
|
||||
"name": "after",
|
||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "before",
|
||||
"description": "Returns the elements in the list that come before the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "first",
|
||||
"description": "Returns the first _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "last",
|
||||
"description": "Returns the last _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SastCiConfigurationEntityConnection",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ The Advanced Search can be useful in various scenarios.
|
|||
### Faster searches
|
||||
|
||||
If you are dealing with huge amount of data and want to keep GitLab's search
|
||||
fast, the Advanced Search will help you achieve that.
|
||||
fast, Advanced Search will help you achieve that.
|
||||
|
||||
NOTE: **Note:**
|
||||
Between versions 12.10 and 13.4, Advanced Search response times have improved by 80%.
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ module Gitlab
|
|||
push_frontend_feature_flag(:snippets_vue, default_enabled: true)
|
||||
push_frontend_feature_flag(:monaco_blobs, default_enabled: true)
|
||||
push_frontend_feature_flag(:monaco_ci, default_enabled: false)
|
||||
push_frontend_feature_flag(:snippets_edit_vue, default_enabled: false)
|
||||
push_frontend_feature_flag(:snippets_edit_vue, default_enabled: true)
|
||||
push_frontend_feature_flag(:webperf_experiment, default_enabled: false)
|
||||
push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false)
|
||||
|
||||
|
|
|
|||
|
|
@ -10374,6 +10374,9 @@ msgstr ""
|
|||
msgid "Expand all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expand all files"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expand approvers"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -11216,6 +11219,9 @@ msgstr ""
|
|||
msgid "Footer message"
|
||||
msgstr ""
|
||||
|
||||
msgid "For a faster browsing experience, some files are collapsed by default."
|
||||
msgstr ""
|
||||
|
||||
msgid "For help setting up the Service Desk for your instance, please contact an administrator."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15385,6 +15391,9 @@ msgstr ""
|
|||
msgid "Max seats used"
|
||||
msgstr ""
|
||||
|
||||
msgid "Max size 15 MB"
|
||||
msgstr ""
|
||||
|
||||
msgid "Maximum Users:"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -21456,6 +21465,9 @@ msgstr ""
|
|||
msgid "Request parameter %{param} is missing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Request review from"
|
||||
msgstr ""
|
||||
|
||||
msgid "Request to link SAML account must be authorized"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -21726,6 +21738,9 @@ msgstr ""
|
|||
msgid "ReviewApp|Enable Review App"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reviewer"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reviewing"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23559,6 +23574,9 @@ msgstr ""
|
|||
msgid "Solution"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some changes are not shown"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some child epics may be hidden due to applied filters"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -58,5 +58,9 @@ FactoryBot.define do
|
|||
started
|
||||
status { 'skipped' }
|
||||
end
|
||||
|
||||
trait :strategy_depend do
|
||||
options { { trigger: { strategy: 'depend' } } }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'Projects > Snippets > Create Snippet', :js do
|
||||
include DropzoneHelper
|
||||
include Spec::Support::Helpers::Features::SnippetSpecHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) do
|
||||
|
|
@ -16,96 +17,115 @@ RSpec.describe 'Projects > Snippets > Create Snippet', :js do
|
|||
let(:file_content) { 'Hello World!' }
|
||||
let(:md_description) { 'My Snippet **Description**' }
|
||||
let(:description) { 'My Snippet Description' }
|
||||
let(:snippet_title_field) { 'project_snippet_title' }
|
||||
|
||||
before do
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
stub_feature_flags(snippets_edit_vue: false)
|
||||
|
||||
sign_in(user)
|
||||
|
||||
visit new_project_snippet_path(project)
|
||||
end
|
||||
|
||||
def description_field
|
||||
find('.js-description-input').find('input,textarea')
|
||||
end
|
||||
|
||||
def fill_form
|
||||
fill_in 'project_snippet_title', with: title
|
||||
|
||||
# Click placeholder first to expand full description field
|
||||
description_field.click
|
||||
fill_in 'project_snippet_description', with: md_description
|
||||
|
||||
page.within('.file-editor') do
|
||||
el = find('.inputarea')
|
||||
el.send_keys file_content
|
||||
shared_examples 'snippet creation' do
|
||||
def fill_form
|
||||
snippet_fill_in_form(title: title, content: file_content, description: md_description)
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows collapsible description input' do
|
||||
collapsed = description_field
|
||||
it 'shows collapsible description input' do
|
||||
collapsed = description_field
|
||||
|
||||
expect(page).not_to have_field('project_snippet_description')
|
||||
expect(collapsed).to be_visible
|
||||
expect(page).not_to have_field(snippet_description_field)
|
||||
expect(collapsed).to be_visible
|
||||
|
||||
collapsed.click
|
||||
collapsed.click
|
||||
|
||||
expect(page).to have_field('project_snippet_description')
|
||||
expect(collapsed).not_to be_visible
|
||||
end
|
||||
|
||||
it 'creates a new snippet' do
|
||||
fill_form
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content(title)
|
||||
expect(page).to have_content(file_content)
|
||||
page.within('.snippet-header .description') do
|
||||
expect(page).to have_content(description)
|
||||
expect(page).to have_selector('strong')
|
||||
expect(page).to have_field(snippet_description_field)
|
||||
expect(collapsed).not_to be_visible
|
||||
end
|
||||
end
|
||||
|
||||
it 'uploads a file when dragging into textarea' do
|
||||
fill_form
|
||||
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
|
||||
|
||||
expect(page.find_field('project_snippet_description').value).to have_content('banana_sample')
|
||||
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
|
||||
expect(link).to match(%r{/#{Regexp.escape(project.full_path)}/uploads/\h{32}/banana_sample\.gif\z})
|
||||
end
|
||||
|
||||
it 'displays validation errors' do
|
||||
fill_in 'project_snippet_title', with: title
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('#error_explanation')
|
||||
end
|
||||
|
||||
context 'when the git operation fails' do
|
||||
let(:error) { 'Error creating the snippet' }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(Snippets::CreateService) do |instance|
|
||||
allow(instance).to receive(:create_commit).and_raise(StandardError, error)
|
||||
end
|
||||
|
||||
it 'creates a new snippet' do
|
||||
fill_form
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content(title)
|
||||
expect(page).to have_content(file_content)
|
||||
page.within(snippet_description_view_selector) do
|
||||
expect(page).to have_content(description)
|
||||
expect(page).to have_selector('strong')
|
||||
end
|
||||
end
|
||||
|
||||
it 'uploads a file when dragging into textarea' do
|
||||
fill_form
|
||||
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
|
||||
|
||||
expect(snippet_description_value).to have_content('banana_sample')
|
||||
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
|
||||
expect(link).to match(%r{/#{Regexp.escape(project.full_path)}/uploads/\h{32}/banana_sample\.gif\z})
|
||||
end
|
||||
|
||||
it 'renders the new page and displays the error' do
|
||||
expect(page).to have_content(error)
|
||||
expect(page).to have_content('New Snippet')
|
||||
context 'when the git operation fails' do
|
||||
let(:error) { 'Error creating the snippet' }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(Snippets::CreateService) do |instance|
|
||||
allow(instance).to receive(:create_commit).and_raise(StandardError, error)
|
||||
end
|
||||
|
||||
fill_form
|
||||
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'renders the new page and displays the error' do
|
||||
expect(page).to have_content(error)
|
||||
expect(page).to have_content('New Snippet')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Vue application' do
|
||||
let(:snippet_description_field) { 'snippet-description' }
|
||||
let(:snippet_description_view_selector) { '.snippet-header .snippet-description' }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
|
||||
visit new_project_snippet_path(project)
|
||||
end
|
||||
|
||||
it_behaves_like 'snippet creation'
|
||||
|
||||
it 'does not allow submitting the form without title and content' do
|
||||
fill_in snippet_title_field, with: title
|
||||
|
||||
expect(page).not_to have_button('Create snippet')
|
||||
|
||||
snippet_fill_in_form(title: title, content: file_content)
|
||||
expect(page).to have_button('Create snippet')
|
||||
end
|
||||
end
|
||||
|
||||
context 'non-Vue application' do
|
||||
let(:snippet_description_field) { 'project_snippet_description' }
|
||||
let(:snippet_description_view_selector) { '.snippet-header .description' }
|
||||
|
||||
before do
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
stub_feature_flags(snippets_edit_vue: false)
|
||||
|
||||
sign_in(user)
|
||||
|
||||
visit new_project_snippet_path(project)
|
||||
end
|
||||
|
||||
it_behaves_like 'snippet creation'
|
||||
|
||||
it 'displays validation errors' do
|
||||
fill_in snippet_title_field, with: title
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('#error_explanation')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,57 +3,81 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Projects > Snippets > User updates a snippet', :js do
|
||||
include Spec::Support::Helpers::Features::SnippetSpecHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project, namespace: user.namespace) }
|
||||
let_it_be(:snippet, reload: true) { create(:project_snippet, :repository, project: project, author: user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
stub_feature_flags(snippets_edit_vue: false)
|
||||
let(:snippet_title_field) { 'project_snippet_title' }
|
||||
|
||||
def bootstrap_snippet
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
visit(project_snippet_path(project, snippet))
|
||||
page.visit(edit_project_snippet_path(project, snippet))
|
||||
|
||||
page.within('.detail-page-header') do
|
||||
first(:link, 'Edit').click
|
||||
end
|
||||
wait_for_all_requests
|
||||
end
|
||||
|
||||
it 'displays the snippet blob path and content' do
|
||||
blob = snippet.blobs.first
|
||||
shared_examples 'snippet update' do
|
||||
it 'displays the snippet blob path and content' do
|
||||
blob = snippet.blobs.first
|
||||
|
||||
aggregate_failures do
|
||||
expect(page.find_field('project_snippet_file_name').value).to eq blob.path
|
||||
expect(page.find('.file-content')).to have_content(blob.data.strip)
|
||||
expect(page.find('.snippet-file-content', visible: false).value).to eq blob.data
|
||||
aggregate_failures do
|
||||
expect(snippet_get_first_blob_path).to eq blob.path
|
||||
expect(snippet_get_first_blob_value).to have_content(blob.data.strip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates a snippet' do
|
||||
fill_in('project_snippet_title', with: 'Snippet new title')
|
||||
click_button('Save')
|
||||
it 'updates a snippet' do
|
||||
fill_in('project_snippet_title', with: 'Snippet new title')
|
||||
click_button('Save')
|
||||
|
||||
expect(page).to have_content('Snippet new title')
|
||||
end
|
||||
expect(page).to have_content('Snippet new title')
|
||||
end
|
||||
|
||||
context 'when the git operation fails' do
|
||||
before do
|
||||
allow_next_instance_of(Snippets::UpdateService) do |instance|
|
||||
allow(instance).to receive(:create_commit).and_raise(StandardError, 'Error Message')
|
||||
context 'when the git operation fails' do
|
||||
before do
|
||||
allow_next_instance_of(Snippets::UpdateService) do |instance|
|
||||
allow(instance).to receive(:create_commit).and_raise(StandardError, 'Error Message')
|
||||
end
|
||||
|
||||
fill_in(snippet_title_field, with: 'Snippet new title')
|
||||
fill_in(snippet_blob_path_field, match: :first, with: 'new_file_name')
|
||||
|
||||
click_button('Save')
|
||||
end
|
||||
|
||||
fill_in('project_snippet_title', with: 'Snippet new title')
|
||||
fill_in('project_snippet_file_name', with: 'new_file_name')
|
||||
it 'renders edit page and displays the error' do
|
||||
expect(page.find('.flash-container')).to have_content('Error updating the snippet - Error Message')
|
||||
expect(page).to have_content('Edit Snippet')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
click_button('Save')
|
||||
context 'Vue application' do
|
||||
before do
|
||||
bootstrap_snippet
|
||||
end
|
||||
|
||||
it 'renders edit page and displays the error' do
|
||||
expect(page.find('.flash-container span').text).to eq('Error updating the snippet - Error Message')
|
||||
expect(page).to have_content('Edit Snippet')
|
||||
it_behaves_like 'snippet update' do
|
||||
let(:snippet_blob_path_field) { 'snippet_file_name' }
|
||||
let(:snippet_blob_content_selector) { '.file-content' }
|
||||
end
|
||||
end
|
||||
|
||||
context 'non-Vue application' do
|
||||
before do
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
stub_feature_flags(snippets_edit_vue: false)
|
||||
|
||||
bootstrap_snippet
|
||||
end
|
||||
|
||||
it_behaves_like 'snippet update' do
|
||||
let(:snippet_blob_path_field) { 'project_snippet_file_name' }
|
||||
let(:snippet_blob_content_selector) { '.file-content' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ RSpec.shared_examples_for 'snippet editor' do
|
|||
|
||||
before do
|
||||
stub_feature_flags(allow_possible_spam: false)
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
stub_feature_flags(snippets_edit_vue: false)
|
||||
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
|
||||
|
||||
Gitlab::CurrentSettings.update!(
|
||||
|
|
@ -125,5 +123,20 @@ end
|
|||
RSpec.describe 'User creates snippet', :js do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
it_behaves_like "snippet editor"
|
||||
context 'Vue application' do
|
||||
before do
|
||||
stub_feature_flags(snippets_edit_vue: false)
|
||||
end
|
||||
|
||||
it_behaves_like "snippet editor"
|
||||
end
|
||||
|
||||
context 'non-Vue application' do
|
||||
before do
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
stub_feature_flags(snippets_edit_vue: false)
|
||||
end
|
||||
|
||||
it_behaves_like "snippet editor"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'User creates snippet', :js do
|
||||
include DropzoneHelper
|
||||
include Spec::Support::Helpers::Features::SnippetSpecHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
|
|
@ -12,149 +13,163 @@ RSpec.describe 'User creates snippet', :js do
|
|||
let(:md_description) { 'My Snippet **Description**' }
|
||||
let(:description) { 'My Snippet Description' }
|
||||
let(:created_snippet) { Snippet.last }
|
||||
|
||||
before do
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
stub_feature_flags(snippets_edit_vue: false)
|
||||
sign_in(user)
|
||||
end
|
||||
let(:snippet_title_field) { 'personal_snippet_title' }
|
||||
|
||||
def description_field
|
||||
find('.js-description-input').find('input,textarea')
|
||||
end
|
||||
|
||||
def fill_form
|
||||
fill_in 'personal_snippet_title', with: title
|
||||
|
||||
# Click placeholder first to expand full description field
|
||||
description_field.click
|
||||
fill_in 'personal_snippet_description', with: md_description
|
||||
|
||||
page.within('.file-editor') do
|
||||
el = find('.inputarea')
|
||||
el.send_keys file_content
|
||||
shared_examples 'snippet creation' do
|
||||
def fill_form
|
||||
snippet_fill_in_form(title: title, content: file_content, description: md_description)
|
||||
end
|
||||
end
|
||||
|
||||
it 'Authenticated user creates a snippet' do
|
||||
visit new_snippet_path
|
||||
it 'Authenticated user creates a snippet' do
|
||||
fill_form
|
||||
|
||||
fill_form
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content(title)
|
||||
page.within('.snippet-header .description') do
|
||||
expect(page).to have_content(description)
|
||||
expect(page).to have_selector('strong')
|
||||
expect(page).to have_content(title)
|
||||
page.within(snippet_description_view_selector) do
|
||||
expect(page).to have_content(description)
|
||||
expect(page).to have_selector('strong')
|
||||
end
|
||||
expect(page).to have_content(file_content)
|
||||
end
|
||||
expect(page).to have_content(file_content)
|
||||
end
|
||||
|
||||
it 'previews a snippet with file' do
|
||||
visit new_snippet_path
|
||||
it 'uploads a file when dragging into textarea' do
|
||||
fill_form
|
||||
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
|
||||
|
||||
# Click placeholder first to expand full description field
|
||||
description_field.click
|
||||
fill_in 'personal_snippet_description', with: 'My Snippet'
|
||||
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
|
||||
find('.js-md-preview-button').click
|
||||
expect(snippet_description_value).to have_content('banana_sample')
|
||||
|
||||
page.within('#new_personal_snippet .md-preview-holder') do
|
||||
expect(page).to have_content('My Snippet')
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
link = find('a.no-attachment-icon img.js-lazy-loaded[alt="banana_sample"]')['src']
|
||||
expect(link).to match(%r{/uploads/-/system/user/#{user.id}/\h{32}/banana_sample\.gif\z})
|
||||
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
|
||||
|
||||
# Adds a cache buster for checking if the image exists as Selenium is now handling the cached requests
|
||||
# not anymore as requests when they come straight from memory cache.
|
||||
reqs = inspect_requests { visit("#{link}?ran=#{SecureRandom.base64(20)}") }
|
||||
expect(reqs.first.status_code).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
it 'uploads a file when dragging into textarea' do
|
||||
visit new_snippet_path
|
||||
context 'when the git operation fails' do
|
||||
let(:error) { 'Error creating the snippet' }
|
||||
|
||||
fill_form
|
||||
before do
|
||||
allow_next_instance_of(Snippets::CreateService) do |instance|
|
||||
allow(instance).to receive(:create_commit).and_raise(StandardError, error)
|
||||
end
|
||||
|
||||
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
|
||||
|
||||
expect(page.find_field("personal_snippet_description").value).to have_content('banana_sample')
|
||||
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
link = find('a.no-attachment-icon img.js-lazy-loaded[alt="banana_sample"]')['src']
|
||||
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
|
||||
|
||||
reqs = inspect_requests { visit("#{link}?ran=#{SecureRandom.base64(20)}") }
|
||||
expect(reqs.first.status_code).to eq(200)
|
||||
end
|
||||
|
||||
context 'when the git operation fails' do
|
||||
let(:error) { 'Error creating the snippet' }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(Snippets::CreateService) do |instance|
|
||||
allow(instance).to receive(:create_commit).and_raise(StandardError, error)
|
||||
fill_form
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
visit new_snippet_path
|
||||
it 'renders the new page and displays the error' do
|
||||
expect(page).to have_content(error)
|
||||
expect(page).to have_content('New Snippet')
|
||||
|
||||
fill_form
|
||||
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
action = find('form.snippet-form')['action']
|
||||
expect(action).to include("/snippets")
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders the new page and displays the error' do
|
||||
expect(page).to have_content(error)
|
||||
expect(page).to have_content('New Snippet')
|
||||
context 'when snippets default visibility level is restricted' do
|
||||
before do
|
||||
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE],
|
||||
default_snippet_visibility: Gitlab::VisibilityLevel::PRIVATE)
|
||||
end
|
||||
|
||||
action = find('form.snippet-form')['action']
|
||||
expect(action).to match(%r{/snippets\z})
|
||||
it 'creates a snippet using the lowest available visibility level as default' do
|
||||
visit new_snippet_path
|
||||
|
||||
fill_form
|
||||
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.blob-content')).to have_content(file_content)
|
||||
expect(Snippet.last.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'personal snippet with references' do
|
||||
let(:container) { snippet_description_view_selector }
|
||||
let(:md_description) { references }
|
||||
|
||||
subject do
|
||||
fill_form
|
||||
click_button('Create snippet')
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'validation fails for the first time' do
|
||||
visit new_snippet_path
|
||||
context 'Vue application' do
|
||||
let(:snippet_description_field) { 'snippet-description' }
|
||||
let(:snippet_description_view_selector) { '.snippet-header .snippet-description' }
|
||||
|
||||
fill_in 'personal_snippet_title', with: title
|
||||
click_button('Create snippet')
|
||||
|
||||
expect(page).to have_selector('#error_explanation')
|
||||
end
|
||||
|
||||
context 'when snippets default visibility level is restricted' do
|
||||
before do
|
||||
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE],
|
||||
default_snippet_visibility: Gitlab::VisibilityLevel::PRIVATE)
|
||||
sign_in(user)
|
||||
|
||||
visit new_snippet_path
|
||||
end
|
||||
|
||||
it 'creates a snippet using the lowest available visibility level as default' do
|
||||
visit new_snippet_path
|
||||
it_behaves_like 'snippet creation'
|
||||
|
||||
fill_form
|
||||
it 'validation fails for the first time' do
|
||||
fill_in snippet_title_field, with: title
|
||||
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
expect(page).not_to have_button('Create snippet')
|
||||
|
||||
expect(created_snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
|
||||
snippet_fill_in_form(title: title, content: file_content)
|
||||
expect(page).to have_button('Create snippet')
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'personal snippet with references' do
|
||||
let(:container) { '.snippet-header .description' }
|
||||
let(:md_description) { references }
|
||||
context 'non-Vue application' do
|
||||
let(:snippet_description_field) { 'personal_snippet_description' }
|
||||
let(:snippet_description_view_selector) { '.snippet-header .description' }
|
||||
|
||||
before do
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
stub_feature_flags(snippets_edit_vue: false)
|
||||
|
||||
sign_in(user)
|
||||
|
||||
subject do
|
||||
visit new_snippet_path
|
||||
fill_form
|
||||
end
|
||||
|
||||
it_behaves_like 'snippet creation'
|
||||
|
||||
it 'validation fails for the first time' do
|
||||
fill_in snippet_title_field, with: title
|
||||
click_button('Create snippet')
|
||||
|
||||
wait_for_requests
|
||||
expect(page).to have_selector('#error_explanation')
|
||||
end
|
||||
|
||||
it 'previews a snippet with file' do
|
||||
# Click placeholder first to expand full description field
|
||||
description_field.click
|
||||
fill_in snippet_description_field, with: 'My Snippet'
|
||||
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
|
||||
find('.js-md-preview-button').click
|
||||
|
||||
page.within('.md-preview-holder') do
|
||||
expect(page).to have_content('My Snippet')
|
||||
|
||||
link = find('a.no-attachment-icon img.js-lazy-loaded[alt="banana_sample"]')['src']
|
||||
expect(link).to match(%r{/uploads/-/system/user/#{user.id}/\h{32}/banana_sample\.gif\z})
|
||||
|
||||
# Adds a cache buster for checking if the image exists as Selenium is now handling the cached requests
|
||||
# not anymore as requests when they come straight from memory cache.
|
||||
reqs = inspect_requests { visit("#{link}?ran=#{SecureRandom.base64(20)}") }
|
||||
expect(reqs.first.status_code).to eq(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,87 +4,114 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'User edits snippet', :js do
|
||||
include DropzoneHelper
|
||||
include Spec::Support::Helpers::Features::SnippetSpecHelpers
|
||||
|
||||
let_it_be(:file_name) { 'test.rb' }
|
||||
let_it_be(:content) { 'puts "test"' }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:snippet, reload: true) { create(:personal_snippet, :repository, :public, file_name: file_name, content: content, author: user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
stub_feature_flags(snippets_edit_vue: false)
|
||||
let(:snippet_title_field) { 'personal_snippet_title' }
|
||||
|
||||
sign_in(user)
|
||||
shared_examples 'snippet editing' do
|
||||
it 'displays the snippet blob path and content' do
|
||||
blob = snippet.blobs.first
|
||||
|
||||
visit edit_snippet_path(snippet)
|
||||
wait_for_all_requests
|
||||
end
|
||||
|
||||
it 'displays the snippet blob path and content' do
|
||||
blob = snippet.blobs.first
|
||||
|
||||
aggregate_failures do
|
||||
expect(page.find_field('personal_snippet_file_name').value).to eq blob.path
|
||||
expect(page.find('.file-content')).to have_content(blob.data.strip)
|
||||
expect(page.find('.snippet-file-content', visible: false).value).to eq blob.data
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates the snippet' do
|
||||
fill_in 'personal_snippet_title', with: 'New Snippet Title'
|
||||
|
||||
click_button('Save changes')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('New Snippet Title')
|
||||
end
|
||||
|
||||
it 'updates the snippet with files attached' do
|
||||
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
|
||||
expect(page.find_field('personal_snippet_description').value).to have_content('banana_sample')
|
||||
|
||||
click_button('Save changes')
|
||||
wait_for_requests
|
||||
|
||||
link = find('a.no-attachment-icon img:not(.lazy)[alt="banana_sample"]')['src']
|
||||
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{snippet.id}/\h{32}/banana_sample\.gif\z})
|
||||
end
|
||||
|
||||
it 'updates the snippet to make it internal' do
|
||||
choose 'Internal'
|
||||
|
||||
click_button 'Save changes'
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_no_selector('[data-testid="lock-icon"]')
|
||||
expect(page).to have_selector('[data-testid="shield-icon"]')
|
||||
end
|
||||
|
||||
it 'updates the snippet to make it public' do
|
||||
choose 'Public'
|
||||
|
||||
click_button 'Save changes'
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_no_selector('[data-testid="lock-icon"]')
|
||||
expect(page).to have_selector('[data-testid="earth-icon"]')
|
||||
end
|
||||
|
||||
context 'when the git operation fails' do
|
||||
before do
|
||||
allow_next_instance_of(Snippets::UpdateService) do |instance|
|
||||
allow(instance).to receive(:create_commit).and_raise(StandardError, 'Error Message')
|
||||
aggregate_failures do
|
||||
expect(snippet_get_first_blob_path).to eq blob.path
|
||||
expect(snippet_get_first_blob_value).to have_content(blob.data.strip)
|
||||
end
|
||||
end
|
||||
|
||||
fill_in 'personal_snippet_title', with: 'New Snippet Title'
|
||||
fill_in 'personal_snippet_file_name', with: 'new_file_name'
|
||||
it 'updates the snippet' do
|
||||
fill_in snippet_title_field, with: 'New Snippet Title'
|
||||
|
||||
click_button('Save changes')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('New Snippet Title')
|
||||
end
|
||||
|
||||
it 'renders edit page and displays the error' do
|
||||
expect(page.find('.flash-container span').text).to eq('Error updating the snippet - Error Message')
|
||||
expect(page).to have_content('Edit Snippet')
|
||||
it 'updates the snippet with files attached' do
|
||||
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
|
||||
expect(snippet_description_value).to have_content('banana_sample')
|
||||
|
||||
click_button('Save changes')
|
||||
wait_for_requests
|
||||
|
||||
link = find('a.no-attachment-icon img:not(.lazy)[alt="banana_sample"]')['src']
|
||||
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{snippet.id}/\h{32}/banana_sample\.gif\z})
|
||||
end
|
||||
|
||||
it 'updates the snippet to make it internal' do
|
||||
choose 'Internal'
|
||||
|
||||
click_button 'Save changes'
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_no_selector('[data-testid="lock-icon"]')
|
||||
expect(page).to have_selector('[data-testid="shield-icon"]')
|
||||
end
|
||||
|
||||
it 'updates the snippet to make it public' do
|
||||
choose 'Public'
|
||||
|
||||
click_button 'Save changes'
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_no_selector('[data-testid="lock-icon"]')
|
||||
expect(page).to have_selector('[data-testid="earth-icon"]')
|
||||
end
|
||||
|
||||
context 'when the git operation fails' do
|
||||
before do
|
||||
allow_next_instance_of(Snippets::UpdateService) do |instance|
|
||||
allow(instance).to receive(:create_commit).and_raise(StandardError, 'Error Message')
|
||||
end
|
||||
|
||||
fill_in snippet_title_field, with: 'New Snippet Title'
|
||||
fill_in snippet_blob_path_field, with: 'new_file_name', match: :first
|
||||
|
||||
click_button('Save changes')
|
||||
end
|
||||
|
||||
it 'renders edit page and displays the error' do
|
||||
expect(page.find('.flash-container')).to have_content('Error updating the snippet - Error Message')
|
||||
expect(page).to have_content('Edit Snippet')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Vue application' do
|
||||
it_behaves_like 'snippet editing' do
|
||||
let(:snippet_blob_path_field) { 'snippet_file_name' }
|
||||
let(:snippet_blob_content_selector) { '.file-content' }
|
||||
let(:snippet_description_field) { 'snippet-description' }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
|
||||
visit edit_snippet_path(snippet)
|
||||
wait_for_all_requests
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'non-Vue application' do
|
||||
it_behaves_like 'snippet editing' do
|
||||
let(:snippet_blob_path_field) { 'personal_snippet_file_name' }
|
||||
let(:snippet_blob_content_selector) { '.file-content' }
|
||||
let(:snippet_description_field) { 'personal_snippet_description' }
|
||||
|
||||
before do
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
stub_feature_flags(snippets_edit_vue: false)
|
||||
|
||||
sign_in(user)
|
||||
|
||||
visit edit_snippet_path(snippet)
|
||||
wait_for_all_requests
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import NoChanges from '~/diffs/components/no_changes.vue';
|
|||
import DiffFile from '~/diffs/components/diff_file.vue';
|
||||
import CompareVersions from '~/diffs/components/compare_versions.vue';
|
||||
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
|
||||
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
|
||||
import CommitWidget from '~/diffs/components/commit_widget.vue';
|
||||
import TreeList from '~/diffs/components/tree_list.vue';
|
||||
import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants';
|
||||
|
|
@ -22,6 +23,10 @@ const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`;
|
|||
const COMMIT_URL = '[BASE URL]/OLD';
|
||||
const UPDATED_COMMIT_URL = '[BASE URL]/NEW';
|
||||
|
||||
function getCollapsedFilesWarning(wrapper) {
|
||||
return wrapper.find(CollapsedFilesWarning);
|
||||
}
|
||||
|
||||
describe('diffs/components/app', () => {
|
||||
const oldMrTabs = window.mrTabs;
|
||||
let store;
|
||||
|
|
@ -668,24 +673,51 @@ describe('diffs/components/app', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should render hidden files warning if render overflow warning is present', () => {
|
||||
createComponent({}, ({ state }) => {
|
||||
state.diffs.renderOverflowWarning = true;
|
||||
state.diffs.realSize = '5';
|
||||
state.diffs.plainDiffPath = 'plain diff path';
|
||||
state.diffs.emailPatchPath = 'email patch path';
|
||||
state.diffs.size = 1;
|
||||
describe('warnings', () => {
|
||||
describe('hidden files', () => {
|
||||
it('should render hidden files warning if render overflow warning is present', () => {
|
||||
createComponent({}, ({ state }) => {
|
||||
state.diffs.renderOverflowWarning = true;
|
||||
state.diffs.realSize = '5';
|
||||
state.diffs.plainDiffPath = 'plain diff path';
|
||||
state.diffs.emailPatchPath = 'email patch path';
|
||||
state.diffs.size = 1;
|
||||
});
|
||||
|
||||
expect(wrapper.find(HiddenFilesWarning).exists()).toBe(true);
|
||||
expect(wrapper.find(HiddenFilesWarning).props()).toEqual(
|
||||
expect.objectContaining({
|
||||
total: '5',
|
||||
plainDiffPath: 'plain diff path',
|
||||
emailPatchPath: 'email patch path',
|
||||
visible: 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
expect(wrapper.find(HiddenFilesWarning).exists()).toBe(true);
|
||||
expect(wrapper.find(HiddenFilesWarning).props()).toEqual(
|
||||
expect.objectContaining({
|
||||
total: '5',
|
||||
plainDiffPath: 'plain diff path',
|
||||
emailPatchPath: 'email patch path',
|
||||
visible: 1,
|
||||
}),
|
||||
);
|
||||
describe('collapsed files', () => {
|
||||
it('should render the collapsed files warning if there are any collapsed files', () => {
|
||||
createComponent({}, ({ state }) => {
|
||||
state.diffs.diffFiles = [{ viewer: { collapsed: true } }];
|
||||
});
|
||||
|
||||
expect(getCollapsedFilesWarning(wrapper).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not render the collapsed files warning if the user has dismissed the alert already', async () => {
|
||||
createComponent({}, ({ state }) => {
|
||||
state.diffs.diffFiles = [{ viewer: { collapsed: true } }];
|
||||
});
|
||||
|
||||
expect(getCollapsedFilesWarning(wrapper).exists()).toBe(true);
|
||||
|
||||
wrapper.vm.collapsedWarningDismissed = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(getCollapsedFilesWarning(wrapper).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should display commit widget if store has a commit', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
|
||||
import createStore from '~/diffs/store/modules';
|
||||
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
|
||||
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '~/diffs/constants';
|
||||
|
||||
const propsData = {
|
||||
limited: true,
|
||||
mergeable: true,
|
||||
resolutionPath: 'a-path',
|
||||
};
|
||||
const limitedClasses = CENTERED_LIMITED_CONTAINER_CLASSES.split(' ');
|
||||
|
||||
describe('CollapsedFilesWarning', () => {
|
||||
const localVue = createLocalVue();
|
||||
let store;
|
||||
let wrapper;
|
||||
|
||||
localVue.use(Vuex);
|
||||
|
||||
const getAlertActionButton = () =>
|
||||
wrapper.find(CollapsedFilesWarning).find('button.gl-alert-action:first-child');
|
||||
const getAlertCloseButton = () => wrapper.find(CollapsedFilesWarning).find('button');
|
||||
|
||||
const createComponent = (props = {}, { full } = { full: false }) => {
|
||||
const mounter = full ? mount : shallowMount;
|
||||
store = new Vuex.Store({
|
||||
modules: {
|
||||
diffs: createStore(),
|
||||
},
|
||||
});
|
||||
|
||||
wrapper = mounter(CollapsedFilesWarning, {
|
||||
propsData: { ...propsData, ...props },
|
||||
localVue,
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it.each`
|
||||
limited | containerClasses
|
||||
${true} | ${limitedClasses}
|
||||
${false} | ${[]}
|
||||
`(
|
||||
'has the correct container classes when limited is $limited',
|
||||
({ limited, containerClasses }) => {
|
||||
createComponent({ limited });
|
||||
|
||||
expect(wrapper.classes()).toEqual(containerClasses);
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
present | dismissed
|
||||
${false} | ${true}
|
||||
${true} | ${false}
|
||||
`('toggles the alert when dismissed is $dismissed', ({ present, dismissed }) => {
|
||||
createComponent({ dismissed });
|
||||
|
||||
expect(wrapper.find('[data-testid="root"]').exists()).toBe(present);
|
||||
});
|
||||
|
||||
it('dismisses the component when the alert "x" is clicked', async () => {
|
||||
createComponent({}, { full: true });
|
||||
|
||||
expect(wrapper.find('[data-testid="root"]').exists()).toBe(true);
|
||||
|
||||
getAlertCloseButton().element.click();
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find('[data-testid="root"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('triggers the expandAllFiles action when the alert action button is clicked', () => {
|
||||
createComponent({}, { full: true });
|
||||
|
||||
jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(undefined);
|
||||
|
||||
getAlertActionButton().vm.$emit('click');
|
||||
|
||||
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/expandAllFiles', undefined);
|
||||
});
|
||||
});
|
||||
|
|
@ -215,7 +215,7 @@ describe('DiffFile', () => {
|
|||
it('calls handleLoadCollapsedDiff if collapsed changed & file has no lines', done => {
|
||||
jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {});
|
||||
|
||||
vm.file.highlighted_diff_lines = undefined;
|
||||
vm.file.highlighted_diff_lines = [];
|
||||
vm.file.parallel_diff_lines = [];
|
||||
vm.isCollapsed = true;
|
||||
|
||||
|
|
@ -242,8 +242,8 @@ describe('DiffFile', () => {
|
|||
|
||||
jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {});
|
||||
|
||||
vm.file.highlighted_diff_lines = undefined;
|
||||
vm.file.parallel_diff_lines = [];
|
||||
vm.file.highlighted_diff_lines = [];
|
||||
vm.file.parallel_diff_lines = undefined;
|
||||
vm.isCollapsed = true;
|
||||
|
||||
vm.$nextTick()
|
||||
|
|
|
|||
|
|
@ -306,6 +306,38 @@ RSpec.describe IssuablesHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#reviewer_sidebar_data' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject { helper.reviewer_sidebar_data(user, merge_request: merge_request) }
|
||||
|
||||
context 'without merge_request' do
|
||||
let(:merge_request) { nil }
|
||||
|
||||
it 'returns hash of reviewer data' do
|
||||
is_expected.to eql({
|
||||
avatar_url: user.avatar_url,
|
||||
name: user.name,
|
||||
username: user.username
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with merge_request' do
|
||||
let(:merge_request) { build(:merge_request) }
|
||||
|
||||
where(can_merge: [true, false])
|
||||
|
||||
with_them do
|
||||
before do
|
||||
allow(merge_request).to receive(:can_be_merged_by?).and_return(can_merge)
|
||||
end
|
||||
|
||||
it { is_expected.to include({ can_merge: can_merge })}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#issuable_squash_option?' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
|
|
|
|||
|
|
@ -482,6 +482,8 @@ project:
|
|||
- dast_site_profiles
|
||||
- dast_scanner_profiles
|
||||
- dast_sites
|
||||
- dast_site_tokens
|
||||
- dast_site_validations
|
||||
- operations_feature_flags
|
||||
- operations_feature_flags_client
|
||||
- operations_feature_flags_user_lists
|
||||
|
|
@ -689,6 +691,7 @@ epic:
|
|||
- due_date_sourcing_epic
|
||||
- events
|
||||
- resource_label_events
|
||||
- resource_state_events
|
||||
- user_mentions
|
||||
- note_authors
|
||||
- boards_epic_user_preferences
|
||||
|
|
|
|||
|
|
@ -1360,34 +1360,126 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when pipeline is bridge triggered' do
|
||||
before do
|
||||
pipeline.source_bridge = create(:ci_bridge)
|
||||
end
|
||||
def auto_devops_pipelines_completed_total(status)
|
||||
Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines').get(status: status)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'bridge triggered pipeline' do
|
||||
shared_examples 'upstream downstream pipeline' do
|
||||
let!(:source_pipeline) { create(:ci_sources_pipeline, pipeline: downstream_pipeline, source_job: bridge) }
|
||||
let!(:job) { downstream_pipeline.builds.first }
|
||||
|
||||
context 'when source bridge is dependent on pipeline status' do
|
||||
before do
|
||||
allow(pipeline.source_bridge).to receive(:dependent?).and_return(true)
|
||||
end
|
||||
let!(:bridge) { create(:ci_bridge, :strategy_depend, pipeline: upstream_pipeline) }
|
||||
|
||||
it 'schedules the pipeline bridge worker' do
|
||||
expect(::Ci::PipelineBridgeStatusWorker).to receive(:perform_async)
|
||||
expect(::Ci::PipelineBridgeStatusWorker).to receive(:perform_async).with(downstream_pipeline.id)
|
||||
|
||||
pipeline.succeed!
|
||||
downstream_pipeline.succeed!
|
||||
end
|
||||
|
||||
context 'when the downstream pipeline first fails then retries and succeeds' do
|
||||
it 'makes the upstream pipeline successful' do
|
||||
Sidekiq::Testing.inline! { job.drop! }
|
||||
|
||||
expect(downstream_pipeline.reload).to be_failed
|
||||
expect(upstream_pipeline.reload).to be_failed
|
||||
|
||||
Sidekiq::Testing.inline! do
|
||||
new_job = Ci::Build.retry(job, project.users.first)
|
||||
|
||||
expect(downstream_pipeline.reload).to be_running
|
||||
expect(upstream_pipeline.reload).to be_running
|
||||
|
||||
new_job.success!
|
||||
end
|
||||
|
||||
expect(downstream_pipeline.reload).to be_success
|
||||
expect(upstream_pipeline.reload).to be_success
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the downstream pipeline first succeeds then retries and fails' do
|
||||
it 'makes the upstream pipeline failed' do
|
||||
Sidekiq::Testing.inline! { job.success! }
|
||||
|
||||
expect(downstream_pipeline.reload).to be_success
|
||||
expect(upstream_pipeline.reload).to be_success
|
||||
|
||||
Sidekiq::Testing.inline! do
|
||||
new_job = Ci::Build.retry(job, project.users.first)
|
||||
|
||||
expect(downstream_pipeline.reload).to be_running
|
||||
expect(upstream_pipeline.reload).to be_running
|
||||
|
||||
new_job.drop!
|
||||
end
|
||||
|
||||
expect(downstream_pipeline.reload).to be_failed
|
||||
expect(upstream_pipeline.reload).to be_failed
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the upstream pipeline has another dependent upstream pipeline' do
|
||||
let!(:upstream_of_upstream_pipeline) { create(:ci_pipeline) }
|
||||
|
||||
before do
|
||||
upstream_bridge = create(:ci_bridge, :strategy_depend, pipeline: upstream_of_upstream_pipeline)
|
||||
create(:ci_sources_pipeline, pipeline: upstream_pipeline,
|
||||
source_job: upstream_bridge)
|
||||
end
|
||||
|
||||
context 'when the downstream pipeline first fails then retries and succeeds' do
|
||||
it 'makes upstream pipelines successful' do
|
||||
Sidekiq::Testing.inline! { job.drop! }
|
||||
|
||||
expect(downstream_pipeline.reload).to be_failed
|
||||
expect(upstream_pipeline.reload).to be_failed
|
||||
expect(upstream_of_upstream_pipeline.reload).to be_failed
|
||||
|
||||
Sidekiq::Testing.inline! do
|
||||
new_job = Ci::Build.retry(job, project.users.first)
|
||||
|
||||
expect(downstream_pipeline.reload).to be_running
|
||||
expect(upstream_pipeline.reload).to be_running
|
||||
expect(upstream_of_upstream_pipeline.reload).to be_running
|
||||
|
||||
new_job.success!
|
||||
end
|
||||
|
||||
expect(downstream_pipeline.reload).to be_success
|
||||
expect(upstream_pipeline.reload).to be_success
|
||||
expect(upstream_of_upstream_pipeline.reload).to be_success
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when source bridge is not dependent on pipeline status' do
|
||||
let!(:bridge) { create(:ci_bridge, pipeline: upstream_pipeline) }
|
||||
|
||||
it 'does not schedule the pipeline bridge worker' do
|
||||
expect(::Ci::PipelineBridgeStatusWorker).not_to receive(:perform_async)
|
||||
|
||||
pipeline.succeed!
|
||||
downstream_pipeline.succeed!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def auto_devops_pipelines_completed_total(status)
|
||||
Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines').get(status: status)
|
||||
context 'multi-project pipelines' do
|
||||
let!(:downstream_project) { create(:project, :repository) }
|
||||
let!(:upstream_pipeline) { create(:ci_pipeline, project: project) }
|
||||
let!(:downstream_pipeline) { create(:ci_pipeline, :with_job, project: downstream_project) }
|
||||
|
||||
it_behaves_like 'upstream downstream pipeline'
|
||||
end
|
||||
|
||||
context 'parent-child pipelines' do
|
||||
let!(:upstream_pipeline) { create(:ci_pipeline, project: project) }
|
||||
let!(:downstream_pipeline) { create(:ci_pipeline, :with_job, project: project) }
|
||||
|
||||
it_behaves_like 'upstream downstream pipeline'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -3611,4 +3703,65 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'reset_ancestor_bridges!' do
|
||||
context 'when the pipeline is a child pipeline and the bridge is depended' do
|
||||
let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
|
||||
let!(:bridge) { create_bridge(parent_pipeline, pipeline, true) }
|
||||
|
||||
it 'marks source bridge as pending' do
|
||||
pipeline.reset_ancestor_bridges!
|
||||
|
||||
expect(bridge.reload).to be_pending
|
||||
end
|
||||
|
||||
context 'when the parent pipeline has a dependent upstream pipeline' do
|
||||
let!(:upstream_bridge) do
|
||||
create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true)
|
||||
end
|
||||
|
||||
it 'marks all source bridges as pending' do
|
||||
pipeline.reset_ancestor_bridges!
|
||||
|
||||
expect(bridge.reload).to be_pending
|
||||
expect(upstream_bridge.reload).to be_pending
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the pipeline is a child pipeline and the bridge is not depended' do
|
||||
let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
|
||||
let!(:bridge) { create_bridge(parent_pipeline, pipeline, false) }
|
||||
|
||||
it 'does not touch source bridge' do
|
||||
pipeline.reset_ancestor_bridges!
|
||||
|
||||
expect(bridge.reload).to be_success
|
||||
end
|
||||
|
||||
context 'when the parent pipeline has a dependent upstream pipeline' do
|
||||
let!(:upstream_bridge) do
|
||||
create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true)
|
||||
end
|
||||
|
||||
it 'does not touch any source bridge' do
|
||||
pipeline.reset_ancestor_bridges!
|
||||
|
||||
expect(bridge.reload).to be_success
|
||||
expect(upstream_bridge.reload).to be_success
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_bridge(upstream, downstream, depend = false)
|
||||
options = depend ? { trigger: { strategy: 'depend' } } : {}
|
||||
|
||||
bridge = create(:ci_bridge, pipeline: upstream, status: 'success', options: options)
|
||||
create(:ci_sources_pipeline, pipeline: downstream, source_job: bridge)
|
||||
|
||||
bridge
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,4 +11,32 @@ RSpec.describe ResourceStateEvent, type: :model do
|
|||
it_behaves_like 'a resource event'
|
||||
it_behaves_like 'a resource event for issues'
|
||||
it_behaves_like 'a resource event for merge requests'
|
||||
|
||||
describe 'validations' do
|
||||
describe 'Issuable validation' do
|
||||
it 'is valid if an issue is set' do
|
||||
subject.attributes = { issue: build_stubbed(:issue), merge_request: nil }
|
||||
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it 'is valid if a merge request is set' do
|
||||
subject.attributes = { issue: nil, merge_request: build_stubbed(:merge_request) }
|
||||
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it 'is invalid if both issue and merge request are set' do
|
||||
subject.attributes = { issue: build_stubbed(:issue), merge_request: build_stubbed(:merge_request) }
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
end
|
||||
|
||||
it 'is invalid if there is no issuable set' do
|
||||
subject.attributes = { issue: nil, merge_request: nil }
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -232,6 +232,19 @@ RSpec.describe Ci::RetryBuildService do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the pipeline is a child pipeline and the bridge is depended' do
|
||||
let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
|
||||
let!(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let!(:bridge) { create(:ci_bridge, :strategy_depend, pipeline: parent_pipeline, status: 'success') }
|
||||
let!(:source_pipeline) { create(:ci_sources_pipeline, pipeline: pipeline, source_job: bridge) }
|
||||
|
||||
it 'marks source bridge as pending' do
|
||||
service.execute(build)
|
||||
|
||||
expect(bridge.reload).to be_pending
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have ability to execute build' do
|
||||
|
|
|
|||
|
|
@ -280,6 +280,20 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
|
|||
expect(build3.reload.scheduling_type).to eq('dag')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the pipeline is a downstream pipeline and the bridge is depended' do
|
||||
let!(:bridge) { create(:ci_bridge, :strategy_depend, status: 'success') }
|
||||
|
||||
before do
|
||||
create(:ci_sources_pipeline, pipeline: pipeline, source_job: bridge)
|
||||
end
|
||||
|
||||
it 'marks source bridge as pending' do
|
||||
service.execute(pipeline)
|
||||
|
||||
expect(bridge.reload).to be_pending
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not allowed to retry pipeline' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# These helpers help you interact within the Editor Lite (single-file editor, snippets, etc.).
|
||||
#
|
||||
module Spec
|
||||
module Support
|
||||
module Helpers
|
||||
module Features
|
||||
module SnippetSpecHelpers
|
||||
include ActionView::Helpers::JavaScriptHelper
|
||||
include Spec::Support::Helpers::Features::EditorLiteSpecHelpers
|
||||
|
||||
def snippet_get_first_blob_path
|
||||
page.find_field(snippet_blob_path_field, match: :first).value
|
||||
end
|
||||
|
||||
def snippet_get_first_blob_value
|
||||
page.find(snippet_blob_content_selector, match: :first)
|
||||
end
|
||||
|
||||
def snippet_description_value
|
||||
page.find_field(snippet_description_field).value
|
||||
end
|
||||
|
||||
def snippet_fill_in_form(title:, content:, description: '')
|
||||
# fill_in snippet_title_field, with: title
|
||||
# editor_set_value(content)
|
||||
fill_in snippet_title_field, with: title
|
||||
|
||||
if description
|
||||
# Click placeholder first to expand full description field
|
||||
description_field.click
|
||||
fill_in snippet_description_field, with: description
|
||||
end
|
||||
|
||||
page.within('.file-editor') do
|
||||
el = find('.inputarea')
|
||||
el.send_keys content
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def description_field
|
||||
find('.js-description-input').find('input,textarea')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue