Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6462f5216f
commit
347876a78e
|
@ -0,0 +1,3 @@
|
||||||
|
import { initPipelineEditor } from '~/pipeline_editor';
|
||||||
|
|
||||||
|
initPipelineEditor();
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import PipelineEditorApp from './pipeline_editor_app.vue';
|
||||||
|
|
||||||
|
export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
|
||||||
|
const el = document.querySelector(selector);
|
||||||
|
|
||||||
|
return new Vue({
|
||||||
|
el,
|
||||||
|
render(h) {
|
||||||
|
return h(PipelineEditorApp);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script>
|
||||||
|
import { GlEmptyState } from '@gitlab/ui';
|
||||||
|
import { __, s__ } from '~/locale';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GlEmptyState,
|
||||||
|
},
|
||||||
|
i18n: {
|
||||||
|
title: s__('Pipelines|Pipeline Editor'),
|
||||||
|
description: s__(
|
||||||
|
'Pipelines|We are beginning our work around building the foundation for our dedicated pipeline editor.',
|
||||||
|
),
|
||||||
|
primaryButtonText: __('Learn more'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<gl-empty-state
|
||||||
|
:title="$options.i18n.title"
|
||||||
|
:description="$options.i18n.description"
|
||||||
|
:primary-button-text="$options.i18n.primaryButtonText"
|
||||||
|
primary-button-link="https://about.gitlab.com/direction/verify/pipeline_authoring/"
|
||||||
|
/>
|
||||||
|
</template>
|
|
@ -70,34 +70,50 @@ export default {
|
||||||
</span>
|
</span>
|
||||||
<span class="uploading-error-container hide">
|
<span class="uploading-error-container hide">
|
||||||
<span class="uploading-error-icon">
|
<span class="uploading-error-icon">
|
||||||
<template>
|
<gl-icon name="media" />
|
||||||
<gl-icon name="media" />
|
|
||||||
</template>
|
|
||||||
</span>
|
</span>
|
||||||
<span class="uploading-error-message"></span>
|
<span class="uploading-error-message"></span>
|
||||||
|
|
||||||
<gl-sprintf
|
<gl-sprintf
|
||||||
:message="
|
:message="
|
||||||
__(
|
__(
|
||||||
'%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}',
|
'%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}.',
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<template #retryButton="{content}">
|
<template #retryButton="{content}">
|
||||||
<button class="retry-uploading-link" type="button">{{ content }}</button>
|
<gl-button
|
||||||
|
variant="link"
|
||||||
|
category="primary"
|
||||||
|
class="retry-uploading-link gl-vertical-align-baseline"
|
||||||
|
>
|
||||||
|
{{ content }}
|
||||||
|
</gl-button>
|
||||||
</template>
|
</template>
|
||||||
<template #newFileButton="{content}">
|
<template #newFileButton="{content}">
|
||||||
<button class="attach-new-file markdown-selector" type="button">{{ content }}</button>
|
<gl-button
|
||||||
|
variant="link"
|
||||||
|
category="primary"
|
||||||
|
class="markdown-selector attach-new-file gl-vertical-align-baseline"
|
||||||
|
>
|
||||||
|
{{ content }}
|
||||||
|
</gl-button>
|
||||||
</template>
|
</template>
|
||||||
</gl-sprintf>
|
</gl-sprintf>
|
||||||
</span>
|
</span>
|
||||||
<gl-button class="markdown-selector button-attach-file" variant="link">
|
<gl-button
|
||||||
<template>
|
icon="media"
|
||||||
<gl-icon name="media" :size="16" />
|
variant="link"
|
||||||
</template>
|
category="primary"
|
||||||
<span class="text-attach-file">{{ __('Attach a file') }}</span>
|
class="markdown-selector button-attach-file gl-vertical-align-text-bottom"
|
||||||
|
>
|
||||||
|
{{ __('Attach a file') }}
|
||||||
</gl-button>
|
</gl-button>
|
||||||
<gl-button class="btn btn-default btn-sm hide button-cancel-uploading-files" variant="link">
|
<gl-button
|
||||||
|
variant="link"
|
||||||
|
category="primary"
|
||||||
|
class="button-cancel-uploading-files gl-vertical-align-baseline hide"
|
||||||
|
>
|
||||||
{{ __('Cancel') }}
|
{{ __('Cancel') }}
|
||||||
</gl-button>
|
</gl-button>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -3,5 +3,3 @@ export const DropdownVariant = {
|
||||||
Standalone: 'standalone',
|
Standalone: 'standalone',
|
||||||
Embedded: 'embedded',
|
Embedded: 'embedded',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LIST_BUFFER_SIZE = 5;
|
|
||||||
|
|
|
@ -1,23 +1,25 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||||
import { GlLoadingIcon, GlButton, GlSearchBoxByType, GlLink } from '@gitlab/ui';
|
import {
|
||||||
|
GlIntersectionObserver,
|
||||||
|
GlLoadingIcon,
|
||||||
|
GlButton,
|
||||||
|
GlSearchBoxByType,
|
||||||
|
GlLink,
|
||||||
|
} from '@gitlab/ui';
|
||||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||||
|
|
||||||
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
|
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
|
||||||
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
|
|
||||||
|
|
||||||
import LabelItem from './label_item.vue';
|
import LabelItem from './label_item.vue';
|
||||||
|
|
||||||
import { LIST_BUFFER_SIZE } from './constants';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
LIST_BUFFER_SIZE,
|
|
||||||
components: {
|
components: {
|
||||||
|
GlIntersectionObserver,
|
||||||
GlLoadingIcon,
|
GlLoadingIcon,
|
||||||
GlButton,
|
GlButton,
|
||||||
GlSearchBoxByType,
|
GlSearchBoxByType,
|
||||||
GlLink,
|
GlLink,
|
||||||
SmartVirtualList,
|
|
||||||
LabelItem,
|
LabelItem,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -46,15 +48,8 @@ export default {
|
||||||
}
|
}
|
||||||
return this.labels;
|
return this.labels;
|
||||||
},
|
},
|
||||||
showListContainer() {
|
|
||||||
if (this.isDropdownVariantSidebar) {
|
|
||||||
return !this.labelsFetchInProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
showNoMatchingResultsMessage() {
|
showNoMatchingResultsMessage() {
|
||||||
return !this.labelsFetchInProgress && !this.visibleLabels.length;
|
return Boolean(this.searchKey) && this.visibleLabels.length === 0;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -67,14 +62,12 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
this.fetchLabels();
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions([
|
...mapActions([
|
||||||
'toggleDropdownContents',
|
'toggleDropdownContents',
|
||||||
'toggleDropdownContentsCreateView',
|
'toggleDropdownContentsCreateView',
|
||||||
'fetchLabels',
|
'fetchLabels',
|
||||||
|
'receiveLabelsSuccess',
|
||||||
'updateSelectedLabels',
|
'updateSelectedLabels',
|
||||||
'toggleDropdownContents',
|
'toggleDropdownContents',
|
||||||
]),
|
]),
|
||||||
|
@ -99,6 +92,17 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* We want to remove loaded labels to ensure component
|
||||||
|
* fetches fresh set of labels every time when shown.
|
||||||
|
*/
|
||||||
|
handleComponentDisappear() {
|
||||||
|
this.receiveLabelsSuccess([]);
|
||||||
|
},
|
||||||
|
handleCreateLabelClick() {
|
||||||
|
this.receiveLabelsSuccess([]);
|
||||||
|
this.toggleDropdownContentsCreateView();
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* This method enables keyboard navigation support for
|
* This method enables keyboard navigation support for
|
||||||
* the dropdown.
|
* the dropdown.
|
||||||
|
@ -135,84 +139,75 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown">
|
<gl-intersection-observer @appear="fetchLabels" @disappear="handleComponentDisappear">
|
||||||
<gl-loading-icon
|
<div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown">
|
||||||
v-if="labelsFetchInProgress"
|
<div
|
||||||
class="labels-fetch-loading position-absolute gl-display-flex gl-align-items-center w-100 h-100"
|
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
|
||||||
size="md"
|
class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
|
||||||
/>
|
data-testid="dropdown-title"
|
||||||
<div
|
|
||||||
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
|
|
||||||
class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
|
|
||||||
data-testid="dropdown-title"
|
|
||||||
>
|
|
||||||
<span class="flex-grow-1">{{ labelsListTitle }}</span>
|
|
||||||
<gl-button
|
|
||||||
:aria-label="__('Close')"
|
|
||||||
variant="link"
|
|
||||||
size="small"
|
|
||||||
class="dropdown-header-button gl-p-0!"
|
|
||||||
icon="close"
|
|
||||||
@click="toggleDropdownContents"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="dropdown-input" @click.stop="() => {}">
|
|
||||||
<gl-search-box-by-type
|
|
||||||
v-model="searchKey"
|
|
||||||
:autofocus="true"
|
|
||||||
data-qa-selector="dropdown_input_field"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="showListContainer"
|
|
||||||
ref="labelsListContainer"
|
|
||||||
class="dropdown-content"
|
|
||||||
data-testid="dropdown-content"
|
|
||||||
>
|
|
||||||
<smart-virtual-list
|
|
||||||
:length="visibleLabels.length"
|
|
||||||
:remain="$options.LIST_BUFFER_SIZE"
|
|
||||||
:size="$options.LIST_BUFFER_SIZE"
|
|
||||||
wclass="list-unstyled mb-0"
|
|
||||||
wtag="ul"
|
|
||||||
class="h-100"
|
|
||||||
>
|
>
|
||||||
<li v-for="(label, index) in visibleLabels" :key="label.id" class="d-block text-left">
|
<span class="flex-grow-1">{{ labelsListTitle }}</span>
|
||||||
|
<gl-button
|
||||||
|
:aria-label="__('Close')"
|
||||||
|
variant="link"
|
||||||
|
size="small"
|
||||||
|
class="dropdown-header-button gl-p-0!"
|
||||||
|
icon="close"
|
||||||
|
@click="toggleDropdownContents"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-input" @click.stop="() => {}">
|
||||||
|
<gl-search-box-by-type
|
||||||
|
v-model="searchKey"
|
||||||
|
:autofocus="true"
|
||||||
|
:disabled="labelsFetchInProgress"
|
||||||
|
data-qa-selector="dropdown_input_field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div ref="labelsListContainer" class="dropdown-content" data-testid="dropdown-content">
|
||||||
|
<gl-loading-icon
|
||||||
|
v-if="labelsFetchInProgress"
|
||||||
|
class="labels-fetch-loading gl-align-items-center w-100 h-100"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
<ul v-else class="list-unstyled mb-0">
|
||||||
<label-item
|
<label-item
|
||||||
|
v-for="(label, index) in visibleLabels"
|
||||||
|
:key="label.id"
|
||||||
:label="label"
|
:label="label"
|
||||||
:is-label-set="label.set"
|
:is-label-set="label.set"
|
||||||
:highlight="index === currentHighlightItem"
|
:highlight="index === currentHighlightItem"
|
||||||
@clickLabel="handleLabelClick(label)"
|
@clickLabel="handleLabelClick(label)"
|
||||||
/>
|
/>
|
||||||
</li>
|
<li v-show="showNoMatchingResultsMessage" class="gl-p-3 gl-text-center">
|
||||||
<li v-show="showNoMatchingResultsMessage" class="gl-p-3 gl-text-center">
|
{{ __('No matching results') }}
|
||||||
{{ __('No matching results') }}
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
</smart-virtual-list>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
|
||||||
|
class="dropdown-footer"
|
||||||
|
data-testid="dropdown-footer"
|
||||||
|
>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li v-if="allowLabelCreate">
|
||||||
|
<gl-link
|
||||||
|
class="gl-display-flex w-100 flex-row text-break-word label-item"
|
||||||
|
@click="handleCreateLabelClick"
|
||||||
|
>
|
||||||
|
{{ footerCreateLabelTitle }}
|
||||||
|
</gl-link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<gl-link
|
||||||
|
:href="labelsManagePath"
|
||||||
|
class="gl-display-flex flex-row text-break-word label-item"
|
||||||
|
>
|
||||||
|
{{ footerManageLabelTitle }}
|
||||||
|
</gl-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</gl-intersection-observer>
|
||||||
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
|
|
||||||
class="dropdown-footer"
|
|
||||||
data-testid="dropdown-footer"
|
|
||||||
>
|
|
||||||
<ul class="list-unstyled">
|
|
||||||
<li v-if="allowLabelCreate">
|
|
||||||
<gl-link
|
|
||||||
class="gl-display-flex w-100 flex-row text-break-word label-item"
|
|
||||||
@click="toggleDropdownContentsCreateView"
|
|
||||||
>
|
|
||||||
{{ footerCreateLabelTitle }}
|
|
||||||
</gl-link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<gl-link
|
|
||||||
:href="labelsManagePath"
|
|
||||||
class="gl-display-flex flex-row text-break-word label-item"
|
|
||||||
>
|
|
||||||
{{ footerManageLabelTitle }}
|
|
||||||
</gl-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlIcon, GlLink } from '@gitlab/ui';
|
import { GlLink, GlIcon } from '@gitlab/ui';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
functional: true,
|
||||||
GlIcon,
|
|
||||||
GlLink,
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
label: {
|
label: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -21,46 +18,65 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
render(h, { props, listeners }) {
|
||||||
return {
|
const { label, highlight, isLabelSet } = props;
|
||||||
isSet: this.isLabelSet,
|
|
||||||
};
|
const labelColorBox = h('span', {
|
||||||
},
|
class: 'dropdown-label-box',
|
||||||
computed: {
|
style: {
|
||||||
labelBoxStyle() {
|
backgroundColor: label.color,
|
||||||
return {
|
},
|
||||||
backgroundColor: this.label.color,
|
attrs: {
|
||||||
};
|
'data-testid': 'label-color-box',
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
watch: {
|
|
||||||
/**
|
const checkedIcon = h(GlIcon, {
|
||||||
* This watcher assures that if user used
|
class: {
|
||||||
* `Enter` key to set/unset label, changes
|
'mr-2 align-self-center': true,
|
||||||
* are reflected here too.
|
hidden: !isLabelSet,
|
||||||
*/
|
},
|
||||||
isLabelSet(value) {
|
props: {
|
||||||
this.isSet = value;
|
name: 'mobile-issue-close',
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
methods: {
|
|
||||||
handleClick() {
|
const noIcon = h('span', {
|
||||||
this.isSet = !this.isSet;
|
class: {
|
||||||
this.$emit('clickLabel', this.label);
|
'mr-3 pr-2': true,
|
||||||
},
|
hidden: isLabelSet,
|
||||||
|
},
|
||||||
|
attrs: {
|
||||||
|
'data-testid': 'no-icon',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const labelTitle = h('span', label.title);
|
||||||
|
|
||||||
|
const labelLink = h(
|
||||||
|
GlLink,
|
||||||
|
{
|
||||||
|
class: 'd-flex align-items-baseline text-break-word label-item',
|
||||||
|
on: {
|
||||||
|
click: () => {
|
||||||
|
listeners.clickLabel(label);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[noIcon, checkedIcon, labelColorBox, labelTitle],
|
||||||
|
);
|
||||||
|
|
||||||
|
return h(
|
||||||
|
'li',
|
||||||
|
{
|
||||||
|
class: {
|
||||||
|
'd-block': true,
|
||||||
|
'text-left': true,
|
||||||
|
'is-focused': highlight,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[labelLink],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<gl-link
|
|
||||||
class="d-flex align-items-baseline text-break-word label-item"
|
|
||||||
:class="{ 'is-focused': highlight }"
|
|
||||||
@click="handleClick"
|
|
||||||
>
|
|
||||||
<gl-icon v-show="isSet" name="mobile-issue-close" class="mr-2 align-self-center" />
|
|
||||||
<span v-show="!isSet" data-testid="no-icon" class="mr-3 pr-2"></span>
|
|
||||||
<span class="dropdown-label-box" data-testid="label-color-box" :style="labelBoxStyle"></span>
|
|
||||||
<span>{{ label.title }}</span>
|
|
||||||
</gl-link>
|
|
||||||
</template>
|
|
||||||
|
|
|
@ -266,7 +266,7 @@ export default {
|
||||||
</dropdown-value>
|
</dropdown-value>
|
||||||
<dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
|
<dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
|
||||||
<dropdown-contents
|
<dropdown-contents
|
||||||
v-if="dropdownButtonVisible && showDropdownContents"
|
v-show="dropdownButtonVisible && showDropdownContents"
|
||||||
ref="dropdownContents"
|
ref="dropdownContents"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1017,6 +1017,23 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
&:hover,
|
||||||
|
&.is-focused {
|
||||||
|
.label-item {
|
||||||
|
@include dropdown-item-hover;
|
||||||
|
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-select-dropdown-button {
|
||||||
|
.gl-button-text {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.labels-select-dropdown-contents {
|
.labels-select-dropdown-contents {
|
||||||
min-height: $dropdown-min-height;
|
min-height: $dropdown-min-height;
|
||||||
max-height: 330px;
|
max-height: 330px;
|
||||||
|
@ -1050,13 +1067,6 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
|
||||||
|
|
||||||
.label-item {
|
.label-item {
|
||||||
padding: 8px 20px;
|
padding: 8px 20px;
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.is-focused {
|
|
||||||
@include dropdown-item-hover;
|
|
||||||
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-input-container {
|
.color-input-container {
|
||||||
|
|
|
@ -450,31 +450,6 @@ table {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attach-new-file,
|
|
||||||
.button-attach-file,
|
|
||||||
.retry-uploading-link {
|
|
||||||
color: $blue-600;
|
|
||||||
padding: 0;
|
|
||||||
background: none;
|
|
||||||
border: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 16px;
|
|
||||||
vertical-align: initial;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
.text-attach-file {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gl-icon:not(:last-child) {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-selector {
|
.markdown-selector {
|
||||||
color: $blue-600;
|
color: $blue-600;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
|
||||||
|
before_action :check_can_collaborate!
|
||||||
|
|
||||||
|
feature_category :pipeline_authoring
|
||||||
|
|
||||||
|
def show
|
||||||
|
render_404 unless ::Gitlab::Ci::Features.ci_pipeline_editor_page_enabled?(@project)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def check_can_collaborate!
|
||||||
|
render_404 unless can_collaborate_with_project?(@project)
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,15 +2,16 @@
|
||||||
|
|
||||||
module Resolvers
|
module Resolvers
|
||||||
class EchoResolver < BaseResolver
|
class EchoResolver < BaseResolver
|
||||||
|
type ::GraphQL::STRING_TYPE, null: false
|
||||||
description 'Testing endpoint to validate the API with'
|
description 'Testing endpoint to validate the API with'
|
||||||
|
|
||||||
argument :text, GraphQL::STRING_TYPE, required: true,
|
argument :text, GraphQL::STRING_TYPE, required: true,
|
||||||
description: 'Text to echo back'
|
description: 'Text to echo back'
|
||||||
|
|
||||||
def resolve(**args)
|
def resolve(text:)
|
||||||
username = context[:current_user]&.username
|
username = current_user&.username
|
||||||
|
|
||||||
"#{username.inspect} says: #{args[:text]}"
|
"#{username.inspect} says: #{text}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Ci
|
||||||
|
module PipelineEditorHelper
|
||||||
|
include ChecksCollaboration
|
||||||
|
|
||||||
|
def can_view_pipeline_editor?(project)
|
||||||
|
can_collaborate_with_project?(project) &&
|
||||||
|
Gitlab::Ci::Features.ci_pipeline_editor_page_enabled?(project)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -167,7 +167,7 @@
|
||||||
= render_if_exists "layouts/nav/requirements_link", project: @project
|
= render_if_exists "layouts/nav/requirements_link", project: @project
|
||||||
|
|
||||||
- if project_nav_tab? :pipelines
|
- if project_nav_tab? :pipelines
|
||||||
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases], unless: -> { current_path?('projects/pipelines#charts') }) do
|
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], unless: -> { current_path?('projects/pipelines#charts') }) do
|
||||||
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do
|
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do
|
||||||
.nav-icon-container
|
.nav-icon-container
|
||||||
= sprite_icon('rocket')
|
= sprite_icon('rocket')
|
||||||
|
@ -175,7 +175,7 @@
|
||||||
= _('CI / CD')
|
= _('CI / CD')
|
||||||
|
|
||||||
%ul.sidebar-sub-level-items
|
%ul.sidebar-sub-level-items
|
||||||
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases], html_options: { class: "fly-out-top-item" }) do
|
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], html_options: { class: "fly-out-top-item" }) do
|
||||||
= link_to project_pipelines_path(@project) do
|
= link_to project_pipelines_path(@project) do
|
||||||
%strong.fly-out-top-item-name
|
%strong.fly-out-top-item-name
|
||||||
= _('CI / CD')
|
= _('CI / CD')
|
||||||
|
@ -186,6 +186,12 @@
|
||||||
%span
|
%span
|
||||||
= _('Pipelines')
|
= _('Pipelines')
|
||||||
|
|
||||||
|
- if can_view_pipeline_editor?(@project)
|
||||||
|
= nav_link(controller: :pipeline_editor, action: :show) do
|
||||||
|
= link_to project_ci_pipeline_editor_path(@project), title: s_('Pipelines|Editor') do
|
||||||
|
%span
|
||||||
|
= s_('Pipelines|Editor')
|
||||||
|
|
||||||
- if project_nav_tab? :builds
|
- if project_nav_tab? :builds
|
||||||
= nav_link(controller: :jobs) do
|
= nav_link(controller: :jobs) do
|
||||||
= link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
|
= link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
%p{ style: 'font-size:18px; text-align:center; line-height:30px;' }
|
%p{ style: 'font-size:18px; text-align:center; line-height:30px;' }
|
||||||
- project_link = link_to(@project.full_name, project_url(@project), style: "color:#3777b0; text-decoration:none; display:block;")
|
- project_link = link_to(@project.full_name, project_url(@project), style: "color:#3777b0; text-decoration:none; display:block;")
|
||||||
= _('Your CSV export of %{count} from project %{project_link} has been added to this email as an attachment.').html_safe % { count: pluralize(@written_count, type.to_s), project_link: project_link }
|
= _('Your CSV export of %{count} from project %{project_link} has been added to this email as an attachment.').html_safe % { count: pluralize(@written_count, type.to_s.titleize.downcase), project_link: project_link }
|
||||||
- if @truncated
|
- if @truncated
|
||||||
%p
|
%p
|
||||||
= _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, count: @count }
|
= _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, count: @count }
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
- page_title s_('Pipelines|Pipeline Editor')
|
||||||
|
|
||||||
|
#js-pipeline-editor
|
|
@ -23,13 +23,20 @@
|
||||||
= sprite_icon('media', css_class: 'gl-icon gl-vertical-align-text-bottom')
|
= sprite_icon('media', css_class: 'gl-icon gl-vertical-align-text-bottom')
|
||||||
%span.uploading-error-message
|
%span.uploading-error-message
|
||||||
-# Populated by app/assets/javascripts/dropzone_input.js
|
-# Populated by app/assets/javascripts/dropzone_input.js
|
||||||
%button.retry-uploading-link{ type: 'button' }= _("Try again")
|
%button.btn.gl-button.btn-link.gl-vertical-align-baseline.retry-uploading-link
|
||||||
or
|
%span.gl-button-text
|
||||||
%button.attach-new-file.markdown-selector{ type: 'button' }= _("attach a new file")
|
= _("Try again")
|
||||||
|
= _("or")
|
||||||
|
%button.btn.gl-button.btn-link.attach-new-file.markdown-selector.gl-vertical-align-baseline
|
||||||
|
%span.gl-button-text
|
||||||
|
= _("attach a new file")
|
||||||
|
= _(".")
|
||||||
|
|
||||||
%button.btn.markdown-selector.button-attach-file.btn-link{ type: 'button' }
|
%button.btn.gl-button.btn-link.button-attach-file.markdown-selector.button-attach-file.gl-vertical-align-text-bottom
|
||||||
= sprite_icon('media')
|
= sprite_icon('media')
|
||||||
%span.text-attach-file<>
|
%span.gl-button-text
|
||||||
= _("Attach a file")
|
= _("Attach a file")
|
||||||
|
|
||||||
%button.btn.btn-default.btn-sm.hide.button-cancel-uploading-files{ type: 'button' }= _("Cancel")
|
%button.btn.gl-button.btn-link.button-cancel-uploading-files.gl-vertical-align-baseline.hide
|
||||||
|
%span.gl-button-text
|
||||||
|
= _("Cancel")
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add container repositories API
|
||||||
|
merge_request: 46495
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Migrate Bootstrap buttons to GitLab UI buttons for attach a file form actions
|
||||||
|
merge_request: 46041
|
||||||
|
author:
|
||||||
|
type: other
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix Vue Labels Select dropdown keyboard scroll
|
||||||
|
merge_request: 43874
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
name: ci_pipeline_editor_page
|
||||||
|
introduced_by_url:
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/270059
|
||||||
|
type: development
|
||||||
|
group: group::pipeline authoring
|
||||||
|
default_enabled: false
|
|
@ -85,6 +85,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
||||||
|
|
||||||
namespace :ci do
|
namespace :ci do
|
||||||
resource :lint, only: [:show, :create]
|
resource :lint, only: [:show, :create]
|
||||||
|
resource :pipeline_editor, only: [:show], controller: :pipeline_editor, path: 'editor'
|
||||||
resources :daily_build_group_report_results, only: [:index], constraints: { format: /(csv|json)/ }
|
resources :daily_build_group_report_results, only: [:index], constraints: { format: /(csv|json)/ }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,48 @@ Example response:
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Get details of a single repository
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/209916) in GitLab 13.6.
|
||||||
|
|
||||||
|
Get details of a registry repository.
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
GET /registry/repositories/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
| Attribute | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `id` | integer/string | yes | The ID of the registry repository accessible by the authenticated user. |
|
||||||
|
| `tags` | boolean | no | If the parameter is included as `true`, the response includes an array of `"tags"`. |
|
||||||
|
| `tags_count` | boolean | no | If the parameter is included as `true`, the response includes `"tags_count"`. |
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/registry/repositories/2?tags=true&tags_count=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "",
|
||||||
|
"path": "group/project",
|
||||||
|
"project_id": 9,
|
||||||
|
"location": "gitlab.example.com:5000/group/project",
|
||||||
|
"created_at": "2019-01-10T13:38:57.391Z",
|
||||||
|
"cleanup_policy_started_at": "2020-08-17T03:12:35.489Z",
|
||||||
|
"tags_count": 1,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "0.0.1",
|
||||||
|
"path": "group/project:0.0.1",
|
||||||
|
"location": "gitlab.example.com:5000/group/project:0.0.1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Delete registry repository
|
## Delete registry repository
|
||||||
|
|
||||||
Delete a repository in registry.
|
Delete a repository in registry.
|
||||||
|
|
|
@ -161,6 +161,7 @@ module API
|
||||||
mount ::API::Commits
|
mount ::API::Commits
|
||||||
mount ::API::CommitStatuses
|
mount ::API::CommitStatuses
|
||||||
mount ::API::ContainerRegistryEvent
|
mount ::API::ContainerRegistryEvent
|
||||||
|
mount ::API::ContainerRepositories
|
||||||
mount ::API::DeployKeys
|
mount ::API::DeployKeys
|
||||||
mount ::API::DeployTokens
|
mount ::API::DeployTokens
|
||||||
mount ::API::Deployments
|
mount ::API::Deployments
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module API
|
||||||
|
class ContainerRepositories < ::API::Base
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
helpers ::API::Helpers::PackagesHelpers
|
||||||
|
|
||||||
|
before { authenticate! }
|
||||||
|
|
||||||
|
namespace 'registry' do
|
||||||
|
params do
|
||||||
|
requires :id, type: String, desc: 'The ID of a project'
|
||||||
|
end
|
||||||
|
resource :repositories, requirements: { id: /[0-9]*/ } do
|
||||||
|
desc 'Get a container repository' do
|
||||||
|
detail 'This feature was introduced in GitLab 13.6.'
|
||||||
|
success Entities::ContainerRegistry::Repository
|
||||||
|
end
|
||||||
|
params do
|
||||||
|
optional :tags, type: Boolean, default: false, desc: 'Determines if tags should be included'
|
||||||
|
optional :tags_count, type: Boolean, default: false, desc: 'Determines if the tags count should be included'
|
||||||
|
end
|
||||||
|
get ':id' do
|
||||||
|
authorize!(:read_container_image, repository)
|
||||||
|
|
||||||
|
present repository, with: Entities::ContainerRegistry::Repository, tags: params[:tags], tags_count: params[:tags_count], user: current_user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
helpers do
|
||||||
|
def repository
|
||||||
|
strong_memoize(:repository) do
|
||||||
|
ContainerRepository.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,6 +10,8 @@ module API
|
||||||
end
|
end
|
||||||
|
|
||||||
class Repository < Grape::Entity
|
class Repository < Grape::Entity
|
||||||
|
include ::API::Helpers::RelatedResourcesHelpers
|
||||||
|
|
||||||
expose :id
|
expose :id
|
||||||
expose :name
|
expose :name
|
||||||
expose :path
|
expose :path
|
||||||
|
@ -19,6 +21,13 @@ module API
|
||||||
expose :expiration_policy_started_at, as: :cleanup_policy_started_at
|
expose :expiration_policy_started_at, as: :cleanup_policy_started_at
|
||||||
expose :tags_count, if: -> (_, options) { options[:tags_count] }
|
expose :tags_count, if: -> (_, options) { options[:tags_count] }
|
||||||
expose :tags, using: Tag, if: -> (_, options) { options[:tags] }
|
expose :tags, using: Tag, if: -> (_, options) { options[:tags] }
|
||||||
|
expose :delete_api_path, if: ->(object, options) { Ability.allowed?(options[:user], :admin_container_image, object) }
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def delete_api_path
|
||||||
|
expose_url api_v4_projects_registry_repositories_path(repository_id: object.id, id: object.project_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class TagDetails < Tag
|
class TagDetails < Tag
|
||||||
|
|
|
@ -66,6 +66,10 @@ module Gitlab
|
||||||
def self.seed_block_run_before_workflow_rules_enabled?(project)
|
def self.seed_block_run_before_workflow_rules_enabled?(project)
|
||||||
::Feature.enabled?(:ci_seed_block_run_before_workflow_rules, project, default_enabled: false)
|
::Feature.enabled?(:ci_seed_block_run_before_workflow_rules, project, default_enabled: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.ci_pipeline_editor_page_enabled?(project)
|
||||||
|
::Feature.enabled?(:ci_pipeline_editor_page, project, default_enabled: false)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,9 +29,9 @@ module Gitlab
|
||||||
json_decode(data)
|
json_decode(data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def consume_relation(importable_path, key)
|
def consume_relation(importable_path, key, mark_as_consumed: true)
|
||||||
Enumerator.new do |documents|
|
Enumerator.new do |documents|
|
||||||
next unless @consumed_relations.add?("#{importable_path}/#{key}")
|
next if mark_as_consumed && !@consumed_relations.add?("#{importable_path}/#{key}")
|
||||||
|
|
||||||
# This reads from `tree/project/merge_requests.ndjson`
|
# This reads from `tree/project/merge_requests.ndjson`
|
||||||
path = file_path(importable_path, "#{key}.ndjson")
|
path = file_path(importable_path, "#{key}.ndjson")
|
||||||
|
@ -44,11 +44,6 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Move clear logic into main comsume_relation method (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430465330)
|
|
||||||
def clear_consumed_relations
|
|
||||||
@consumed_relations.clear
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def json_decode(string)
|
def json_decode(string)
|
||||||
|
|
|
@ -9,7 +9,6 @@ module Gitlab
|
||||||
|
|
||||||
def initialize(dates)
|
def initialize(dates)
|
||||||
@dates = dates.dup
|
@dates = dates.dup
|
||||||
@dates.flatten!
|
|
||||||
@dates.compact!
|
@dates.compact!
|
||||||
@dates.sort!
|
@dates.sort!
|
||||||
@dates.map! { |date| date.to_time.to_f }
|
@dates.map! { |date| date.to_time.to_f }
|
||||||
|
|
|
@ -30,13 +30,12 @@ module Gitlab
|
||||||
data_hash['due_date'] = date_calculator.calculate_by_closest_date_to_average(data_hash['due_date'].to_time) unless data_hash['due_date'].nil?
|
data_hash['due_date'] = date_calculator.calculate_by_closest_date_to_average(data_hash['due_date'].to_time) unless data_hash['due_date'].nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Move clear logic into main comsume_relation method (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430465330)
|
|
||||||
def dates
|
def dates
|
||||||
unless relation_reader.legacy?
|
return if relation_reader.legacy?
|
||||||
DATE_MODELS.map do |tag|
|
|
||||||
relation_reader.consume_relation(@importable_path, tag).map { |model| model.first['due_date'] }.tap do
|
DATE_MODELS.flat_map do |tag|
|
||||||
relation_reader.clear_consumed_relations
|
relation_reader.consume_relation(@importable_path, tag, mark_as_consumed: false).map do |model|
|
||||||
end
|
model.first['due_date']
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -737,7 +737,7 @@ msgstr[1] ""
|
||||||
msgid "%{reportType} %{status} detected no vulnerabilities."
|
msgid "%{reportType} %{status} detected no vulnerabilities."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}"
|
msgid "%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "%{seconds}s"
|
msgid "%{seconds}s"
|
||||||
|
@ -1047,6 +1047,9 @@ msgstr ""
|
||||||
msgid "- show less"
|
msgid "- show less"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "0 bytes"
|
msgid "0 bytes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -19537,6 +19540,9 @@ msgstr ""
|
||||||
msgid "Pipelines|Edit"
|
msgid "Pipelines|Edit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Pipelines|Editor"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Pipelines|Get started with Pipelines"
|
msgid "Pipelines|Get started with Pipelines"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -19567,6 +19573,9 @@ msgstr ""
|
||||||
msgid "Pipelines|Owner"
|
msgid "Pipelines|Owner"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Pipelines|Pipeline Editor"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Pipelines|Project cache successfully reset."
|
msgid "Pipelines|Project cache successfully reset."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -19603,6 +19612,9 @@ msgstr ""
|
||||||
msgid "Pipelines|Trigger user has insufficient permissions to project"
|
msgid "Pipelines|Trigger user has insufficient permissions to project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Pipelines|We are beginning our work around building the foundation for our dedicated pipeline editor."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Pipelines|invalid"
|
msgid "Pipelines|invalid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Projects::Ci::PipelineEditorController do
|
||||||
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #show' do
|
||||||
|
context 'with enough privileges' do
|
||||||
|
before do
|
||||||
|
project.add_developer(user)
|
||||||
|
|
||||||
|
get :show, params: { namespace_id: project.namespace, project_id: project }
|
||||||
|
end
|
||||||
|
|
||||||
|
it { expect(response).to have_gitlab_http_status(:ok) }
|
||||||
|
|
||||||
|
it 'renders show page' do
|
||||||
|
expect(response).to render_template :show
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without enough privileges' do
|
||||||
|
before do
|
||||||
|
project.add_reporter(user)
|
||||||
|
|
||||||
|
get :show, params: { namespace_id: project.namespace, project_id: project }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'responds with 404' do
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ci_pipeline_editor_page feature flag is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_pipeline_editor_page: false)
|
||||||
|
project.add_developer(user)
|
||||||
|
|
||||||
|
get :show, params: { namespace_id: project.namespace, project_id: project }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'responds with 404' do
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -95,7 +95,7 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
|
||||||
expect(page).to have_link('New issue')
|
expect(page).to have_link('New issue')
|
||||||
expect(page).not_to have_button('Close issue')
|
expect(page).not_to have_button('Close issue')
|
||||||
expect(page).not_to have_button('Reopen issue')
|
expect(page).not_to have_button('Reopen issue')
|
||||||
expect(page).not_to have_link('Edit')
|
expect(page).not_to have_link(title: 'Edit title and description')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -121,7 +121,7 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
|
||||||
|
|
||||||
it 'shows only the `Report abuse` and `Edit` button' do
|
it 'shows only the `Report abuse` and `Edit` button' do
|
||||||
expect(page).to have_link('Report abuse')
|
expect(page).to have_link('Report abuse')
|
||||||
expect(page).to have_link('Edit')
|
expect(page).to have_link(exact_text: 'Edit')
|
||||||
expect(page).not_to have_button('Close merge request')
|
expect(page).not_to have_button('Close merge request')
|
||||||
expect(page).not_to have_button('Reopen merge request')
|
expect(page).not_to have_button('Reopen merge request')
|
||||||
end
|
end
|
||||||
|
@ -130,8 +130,8 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
|
||||||
let(:issuable) { create(:merge_request, :merged, source_project: project, author: user) }
|
let(:issuable) { create(:merge_request, :merged, source_project: project, author: user) }
|
||||||
|
|
||||||
it 'shows only the `Edit` button' do
|
it 'shows only the `Edit` button' do
|
||||||
expect(page).to have_link('Edit')
|
|
||||||
expect(page).to have_link('Report abuse')
|
expect(page).to have_link('Report abuse')
|
||||||
|
expect(page).to have_link(exact_text: 'Edit')
|
||||||
expect(page).not_to have_button('Close merge request')
|
expect(page).not_to have_button('Close merge request')
|
||||||
expect(page).not_to have_button('Reopen merge request')
|
expect(page).not_to have_button('Reopen merge request')
|
||||||
end
|
end
|
||||||
|
@ -153,7 +153,7 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
|
||||||
expect(page).to have_link('Report abuse')
|
expect(page).to have_link('Report abuse')
|
||||||
expect(page).not_to have_button('Close merge request')
|
expect(page).not_to have_button('Close merge request')
|
||||||
expect(page).not_to have_button('Reopen merge request')
|
expect(page).not_to have_button('Reopen merge request')
|
||||||
expect(page).not_to have_link('Edit')
|
expect(page).not_to have_link(exact_text: 'Edit')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Pipeline Editor', :js do
|
||||||
|
include Spec::Support::Helpers::Features::EditorLiteSpecHelpers
|
||||||
|
|
||||||
|
let(:project) { create(:project, :repository) }
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
project.add_developer(user)
|
||||||
|
|
||||||
|
visit project_ci_pipeline_editor_path(project)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'user sees the Pipeline Editor page' do
|
||||||
|
expect(page).to have_content('Pipeline Editor')
|
||||||
|
end
|
||||||
|
end
|
|
@ -58,8 +58,8 @@ RSpec.describe 'User uploads file to note' do
|
||||||
error_text = 'File is too big (0.06MiB). Max filesize: 0.01MiB.'
|
error_text = 'File is too big (0.06MiB). Max filesize: 0.01MiB.'
|
||||||
|
|
||||||
expect(page).to have_selector('.uploading-error-message', visible: true, text: error_text)
|
expect(page).to have_selector('.uploading-error-message', visible: true, text: error_text)
|
||||||
expect(page).to have_selector('.retry-uploading-link', visible: true, text: 'Try again')
|
expect(page).to have_button('Try again', visible: true)
|
||||||
expect(page).to have_selector('.attach-new-file', visible: true, text: 'attach a new file')
|
expect(page).to have_button('attach a new file', visible: true)
|
||||||
expect(page).not_to have_button('Attach a file')
|
expect(page).not_to have_button('Attach a file')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { mount, shallowMount } from '@vue/test-utils';
|
||||||
|
import { GlEmptyState } from '@gitlab/ui';
|
||||||
|
|
||||||
|
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
|
||||||
|
|
||||||
|
describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
const createComponent = (mountFn = shallowMount) => {
|
||||||
|
wrapper = mountFn(PipelineEditorApp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findEmptyState = () => wrapper.find(GlEmptyState);
|
||||||
|
|
||||||
|
it('contains an empty state', () => {
|
||||||
|
createComponent();
|
||||||
|
|
||||||
|
expect(findEmptyState().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains a text description', () => {
|
||||||
|
createComponent(mount);
|
||||||
|
|
||||||
|
expect(findEmptyState().text()).toMatchInterpolatedText(
|
||||||
|
'Pipeline Editor We are beginning our work around building the foundation for our dedicated pipeline editor. Learn more',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,9 +1,14 @@
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
|
|
||||||
import { GlButton, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
|
import {
|
||||||
|
GlIntersectionObserver,
|
||||||
|
GlButton,
|
||||||
|
GlLoadingIcon,
|
||||||
|
GlSearchBoxByType,
|
||||||
|
GlLink,
|
||||||
|
} from '@gitlab/ui';
|
||||||
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
|
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
|
||||||
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
|
|
||||||
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue';
|
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue';
|
||||||
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
|
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
|
||||||
|
|
||||||
|
@ -88,20 +93,25 @@ describe('DropdownContentsLabelsView', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('showListContainer', () => {
|
describe('showNoMatchingResultsMessage', () => {
|
||||||
it.each`
|
it.each`
|
||||||
variant | loading | showList
|
searchKey | labels | labelsDescription | returnValue
|
||||||
${'sidebar'} | ${false} | ${true}
|
${''} | ${[]} | ${'empty'} | ${false}
|
||||||
${'sidebar'} | ${true} | ${false}
|
${'bug'} | ${[]} | ${'empty'} | ${true}
|
||||||
${'not-sidebar'} | ${true} | ${true}
|
${''} | ${mockLabels} | ${'not empty'} | ${false}
|
||||||
${'not-sidebar'} | ${false} | ${true}
|
${'bug'} | ${mockLabels} | ${'not empty'} | ${false}
|
||||||
`(
|
`(
|
||||||
'returns $showList if `state.variant` is "$variant" and `labelsFetchInProgress` is $loading',
|
'returns $returnValue when searchKey is "$searchKey" and visibleLabels is $labelsDescription',
|
||||||
({ variant, loading, showList }) => {
|
async ({ searchKey, labels, returnValue }) => {
|
||||||
createComponent({ ...mockConfig, variant });
|
wrapper.setData({
|
||||||
wrapper.vm.$store.state.labelsFetchInProgress = loading;
|
searchKey,
|
||||||
|
});
|
||||||
|
|
||||||
expect(wrapper.vm.showListContainer).toBe(showList);
|
wrapper.vm.$store.dispatch('receiveLabelsSuccess', labels);
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.showNoMatchingResultsMessage).toBe(returnValue);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -118,6 +128,28 @@ describe('DropdownContentsLabelsView', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('handleComponentDisappear', () => {
|
||||||
|
it('calls action `receiveLabelsSuccess` with empty array', () => {
|
||||||
|
jest.spyOn(wrapper.vm, 'receiveLabelsSuccess');
|
||||||
|
|
||||||
|
wrapper.vm.handleComponentDisappear();
|
||||||
|
|
||||||
|
expect(wrapper.vm.receiveLabelsSuccess).toHaveBeenCalledWith([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleCreateLabelClick', () => {
|
||||||
|
it('calls actions `receiveLabelsSuccess` with empty array and `toggleDropdownContentsCreateView`', () => {
|
||||||
|
jest.spyOn(wrapper.vm, 'receiveLabelsSuccess');
|
||||||
|
jest.spyOn(wrapper.vm, 'toggleDropdownContentsCreateView');
|
||||||
|
|
||||||
|
wrapper.vm.handleCreateLabelClick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.receiveLabelsSuccess).toHaveBeenCalledWith([]);
|
||||||
|
expect(wrapper.vm.toggleDropdownContentsCreateView).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('handleKeyDown', () => {
|
describe('handleKeyDown', () => {
|
||||||
it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', () => {
|
it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', () => {
|
||||||
wrapper.setData({
|
wrapper.setData({
|
||||||
|
@ -226,8 +258,8 @@ describe('DropdownContentsLabelsView', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('template', () => {
|
describe('template', () => {
|
||||||
it('renders component container element with class `labels-select-contents-list`', () => {
|
it('renders gl-intersection-observer as component root', () => {
|
||||||
expect(wrapper.attributes('class')).toContain('labels-select-contents-list');
|
expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', () => {
|
it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', () => {
|
||||||
|
@ -272,15 +304,11 @@ describe('DropdownContentsLabelsView', () => {
|
||||||
expect(searchInputEl.attributes('autofocus')).toBe('true');
|
expect(searchInputEl.attributes('autofocus')).toBe('true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders smart-virtual-list element', () => {
|
|
||||||
expect(wrapper.find(SmartVirtualList).exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders label elements for all labels', () => {
|
it('renders label elements for all labels', () => {
|
||||||
expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length);
|
expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders label element with "is-focused" when value of `currentHighlightItem` is more than -1', () => {
|
it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', () => {
|
||||||
wrapper.setData({
|
wrapper.setData({
|
||||||
currentHighlightItem: 0,
|
currentHighlightItem: 0,
|
||||||
});
|
});
|
||||||
|
@ -288,7 +316,7 @@ describe('DropdownContentsLabelsView', () => {
|
||||||
return wrapper.vm.$nextTick(() => {
|
return wrapper.vm.$nextTick(() => {
|
||||||
const labelItemEl = findDropdownContent().find(LabelItem);
|
const labelItemEl = findDropdownContent().find(LabelItem);
|
||||||
|
|
||||||
expect(labelItemEl.props('highlight')).toBe(true);
|
expect(labelItemEl.attributes('highlight')).toBe('true');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -310,9 +338,12 @@ describe('DropdownContentsLabelsView', () => {
|
||||||
|
|
||||||
return wrapper.vm.$nextTick(() => {
|
return wrapper.vm.$nextTick(() => {
|
||||||
const dropdownContent = findDropdownContent();
|
const dropdownContent = findDropdownContent();
|
||||||
|
const loadingIcon = findLoadingIcon();
|
||||||
|
|
||||||
expect(dropdownContent.exists()).toBe(true);
|
expect(dropdownContent.exists()).toBe(true);
|
||||||
expect(dropdownContent.isVisible()).toBe(false);
|
expect(dropdownContent.isVisible()).toBe(true);
|
||||||
|
expect(loadingIcon.exists()).toBe(true);
|
||||||
|
expect(loadingIcon.isVisible()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,15 @@ import { mockRegularLabel } from './mock_data';
|
||||||
|
|
||||||
const mockLabel = { ...mockRegularLabel, set: true };
|
const mockLabel = { ...mockRegularLabel, set: true };
|
||||||
|
|
||||||
const createComponent = ({ label = mockLabel, highlight = true } = {}) =>
|
const createComponent = ({
|
||||||
|
label = mockLabel,
|
||||||
|
isLabelSet = mockLabel.set,
|
||||||
|
highlight = true,
|
||||||
|
} = {}) =>
|
||||||
shallowMount(LabelItem, {
|
shallowMount(LabelItem, {
|
||||||
propsData: {
|
propsData: {
|
||||||
label,
|
label,
|
||||||
isLabelSet: label.set,
|
isLabelSet,
|
||||||
highlight,
|
highlight,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -26,94 +30,44 @@ describe('LabelItem', () => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('computed', () => {
|
|
||||||
describe('labelBoxStyle', () => {
|
|
||||||
it('returns an object containing `backgroundColor` based on `label` prop', () => {
|
|
||||||
expect(wrapper.vm.labelBoxStyle).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
backgroundColor: mockLabel.color,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('watchers', () => {
|
|
||||||
describe('isLabelSet', () => {
|
|
||||||
it('sets value of `isLabelSet` to `isSet` data prop', () => {
|
|
||||||
expect(wrapper.vm.isSet).toBe(true);
|
|
||||||
|
|
||||||
wrapper.setProps({
|
|
||||||
isLabelSet: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return wrapper.vm.$nextTick(() => {
|
|
||||||
expect(wrapper.vm.isSet).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('methods', () => {
|
|
||||||
describe('handleClick', () => {
|
|
||||||
it('sets value of `isSet` data prop to opposite of its current value', () => {
|
|
||||||
wrapper.setData({
|
|
||||||
isSet: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapper.vm.handleClick();
|
|
||||||
expect(wrapper.vm.isSet).toBe(false);
|
|
||||||
wrapper.vm.handleClick();
|
|
||||||
expect(wrapper.vm.isSet).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('emits event `clickLabel` on component with `label` prop as param', () => {
|
|
||||||
wrapper.vm.handleClick();
|
|
||||||
|
|
||||||
expect(wrapper.emitted('clickLabel')).toBeTruthy();
|
|
||||||
expect(wrapper.emitted('clickLabel')[0]).toEqual([mockLabel]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('template', () => {
|
describe('template', () => {
|
||||||
it('renders gl-link component', () => {
|
it('renders gl-link component', () => {
|
||||||
expect(wrapper.find(GlLink).exists()).toBe(true);
|
expect(wrapper.find(GlLink).exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders gl-link component with class `is-focused` when `highlight` prop is true', () => {
|
it('renders component root with class `is-focused` when `highlight` prop is true', () => {
|
||||||
wrapper.setProps({
|
const wrapperTemp = createComponent({
|
||||||
highlight: true,
|
highlight: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return wrapper.vm.$nextTick(() => {
|
expect(wrapperTemp.classes()).toContain('is-focused');
|
||||||
expect(wrapper.find(GlLink).classes()).toContain('is-focused');
|
|
||||||
});
|
wrapperTemp.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders visible gl-icon component when `isSet` prop is true', () => {
|
it('renders visible gl-icon component when `isLabelSet` prop is true', () => {
|
||||||
wrapper.setData({
|
const wrapperTemp = createComponent({
|
||||||
isSet: true,
|
isLabelSet: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return wrapper.vm.$nextTick(() => {
|
const iconEl = wrapperTemp.find(GlIcon);
|
||||||
const iconEl = wrapper.find(GlIcon);
|
|
||||||
|
|
||||||
expect(iconEl.isVisible()).toBe(true);
|
expect(iconEl.isVisible()).toBe(true);
|
||||||
expect(iconEl.props('name')).toBe('mobile-issue-close');
|
expect(iconEl.props('name')).toBe('mobile-issue-close');
|
||||||
});
|
|
||||||
|
wrapperTemp.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders visible span element as placeholder instead of gl-icon when `isSet` prop is false', () => {
|
it('renders visible span element as placeholder instead of gl-icon when `isLabelSet` prop is false', () => {
|
||||||
wrapper.setData({
|
const wrapperTemp = createComponent({
|
||||||
isSet: false,
|
isLabelSet: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return wrapper.vm.$nextTick(() => {
|
const placeholderEl = wrapperTemp.find('[data-testid="no-icon"]');
|
||||||
const placeholderEl = wrapper.find('[data-testid="no-icon"]');
|
|
||||||
|
|
||||||
expect(placeholderEl.isVisible()).toBe(true);
|
expect(placeholderEl.isVisible()).toBe(true);
|
||||||
});
|
|
||||||
|
wrapperTemp.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders label color element', () => {
|
it('renders label color element', () => {
|
||||||
|
|
|
@ -8,6 +8,13 @@ RSpec.describe Resolvers::EchoResolver do
|
||||||
let(:current_user) { create(:user) }
|
let(:current_user) { create(:user) }
|
||||||
let(:text) { 'Message test' }
|
let(:text) { 'Message test' }
|
||||||
|
|
||||||
|
specify do
|
||||||
|
expect(described_class.field_options).to include(
|
||||||
|
type: eq(::GraphQL::STRING_TYPE),
|
||||||
|
null: be_falsey
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
describe '#resolve' do
|
describe '#resolve' do
|
||||||
it 'echoes text and username' do
|
it 'echoes text and username' do
|
||||||
expect(resolve_echo(text)).to eq %Q("#{current_user.username}" says: #{text})
|
expect(resolve_echo(text)).to eq %Q("#{current_user.username}" says: #{text})
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Ci::PipelineEditorHelper do
|
||||||
|
let_it_be(:project) { create(:project) }
|
||||||
|
|
||||||
|
describe 'can_view_pipeline_editor?' do
|
||||||
|
subject { helper.can_view_pipeline_editor?(project) }
|
||||||
|
|
||||||
|
it 'user can view editor if they can collaborate' do
|
||||||
|
allow(helper).to receive(:can_collaborate_with_project?).and_return(true)
|
||||||
|
|
||||||
|
expect(subject).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'user can not view editor if they cannot collaborate' do
|
||||||
|
allow(helper).to receive(:can_collaborate_with_project?).and_return(false)
|
||||||
|
|
||||||
|
expect(subject).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'user can not view editor if feature is disabled' do
|
||||||
|
allow(helper).to receive(:can_collaborate_with_project?).and_return(true)
|
||||||
|
stub_feature_flags(ci_pipeline_editor_page: false)
|
||||||
|
|
||||||
|
expect(subject).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -67,6 +67,14 @@ RSpec.describe Gitlab::ImportExport::JSON::NdjsonReader do
|
||||||
it 'yields nothing to the Enumerator' do
|
it 'yields nothing to the Enumerator' do
|
||||||
expect(subject.to_a).to eq([])
|
expect(subject.to_a).to eq([])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with mark_as_consumed: false' do
|
||||||
|
subject { ndjson_reader.consume_relation(importable_path, key, mark_as_consumed: false) }
|
||||||
|
|
||||||
|
it 'yields every relation value to the Enumerator' do
|
||||||
|
expect(subject.count).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'key has not been consumed' do
|
context 'key has not been consumed' do
|
||||||
|
@ -102,14 +110,4 @@ RSpec.describe Gitlab::ImportExport::JSON::NdjsonReader do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#clear_consumed_relations' do
|
|
||||||
let(:dir_path) { fixture }
|
|
||||||
|
|
||||||
subject { ndjson_reader.clear_consumed_relations }
|
|
||||||
|
|
||||||
it 'returns empty set' do
|
|
||||||
expect(subject).to be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ RSpec.describe Gitlab::ImportExport::Project::Sample::DateCalculator do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when dates are not empty' do
|
context 'when dates are not empty' do
|
||||||
let(:dates) { [[nil, '2020-01-01 00:00:00 +0000'], [nil, '2021-01-01 00:00:00 +0000'], [nil, '2022-01-01 23:59:59 +0000']] }
|
let(:dates) { [nil, '2020-01-01 00:00:00 +0000', '2021-01-01 00:00:00 +0000', nil, '2022-01-01 23:59:59 +0000'] }
|
||||||
|
|
||||||
it { is_expected.to eq(Time.zone.parse('2021-01-01 00:00:00 +0000')) }
|
it { is_expected.to eq(Time.zone.parse('2021-01-01 00:00:00 +0000')) }
|
||||||
end
|
end
|
||||||
|
|
|
@ -52,7 +52,8 @@ RSpec.describe Emails::MergeRequests do
|
||||||
|
|
||||||
it { expect(subject.subject).to eq("#{project.name} | Exported merge requests") }
|
it { expect(subject.subject).to eq("#{project.name} | Exported merge requests") }
|
||||||
it { expect(subject.to).to contain_exactly(user.notification_email_for(project.group)) }
|
it { expect(subject.to).to contain_exactly(user.notification_email_for(project.group)) }
|
||||||
it { expect(subject).to have_content('Your CSV export of 10 merge requests from project')}
|
it { expect(subject.html_part).to have_content("Your CSV export of 10 merge requests from project") }
|
||||||
|
it { expect(subject.text_part).to have_content("Your CSV export of 10 merge requests from project") }
|
||||||
|
|
||||||
context 'when truncated' do
|
context 'when truncated' do
|
||||||
let(:export_status) do
|
let(:export_status) do
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe API::ContainerRepositories do
|
||||||
|
let_it_be(:project) { create(:project, :private) }
|
||||||
|
let_it_be(:reporter) { create(:user) }
|
||||||
|
let_it_be(:guest) { create(:user) }
|
||||||
|
let_it_be(:repository) { create(:container_repository, project: project) }
|
||||||
|
|
||||||
|
let(:users) do
|
||||||
|
{
|
||||||
|
anonymous: nil,
|
||||||
|
guest: guest,
|
||||||
|
reporter: reporter
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:api_user) { reporter }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_reporter(reporter)
|
||||||
|
project.add_guest(guest)
|
||||||
|
|
||||||
|
stub_container_registry_config(enabled: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /registry/repositories/:id' do
|
||||||
|
let(:url) { "/registry/repositories/#{repository.id}" }
|
||||||
|
|
||||||
|
subject { get api(url, api_user) }
|
||||||
|
|
||||||
|
it_behaves_like 'rejected container repository access', :guest, :forbidden
|
||||||
|
it_behaves_like 'rejected container repository access', :anonymous, :unauthorized
|
||||||
|
|
||||||
|
context 'for allowed user' do
|
||||||
|
it 'returns a repository' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(json_response['id']).to eq(repository.id)
|
||||||
|
expect(response.body).not_to include('tags')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a matching schema' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(response).to match_response_schema('registry/repository')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with tags param' do
|
||||||
|
let(:url) { "/registry/repositories/#{repository.id}?tags=true" }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_container_registry_tags(repository: repository.path, tags: %w(rootA latest), with_manifest: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a repository and its tags' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(json_response['id']).to eq(repository.id)
|
||||||
|
expect(response.body).to include('tags')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with tags_count param' do
|
||||||
|
let(:url) { "/registry/repositories/#{repository.id}?tags_count=true" }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_container_registry_tags(repository: repository.path, tags: %w(rootA latest), with_manifest: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a repository and its tags_count' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response.body).to include('tags_count')
|
||||||
|
expect(json_response['tags_count']).to eq(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid repository id' do
|
||||||
|
let(:url) { "/registry/repositories/#{non_existing_record_id}" }
|
||||||
|
|
||||||
|
it_behaves_like 'returning response status', :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,7 +4,7 @@ require 'spec_helper'
|
||||||
RSpec.describe 'GraphQL' do
|
RSpec.describe 'GraphQL' do
|
||||||
include GraphqlHelpers
|
include GraphqlHelpers
|
||||||
|
|
||||||
let(:query) { graphql_query_for('echo', 'text' => 'Hello world' ) }
|
let(:query) { graphql_query_for('echo', text: 'Hello world' ) }
|
||||||
|
|
||||||
context 'logging' do
|
context 'logging' do
|
||||||
shared_examples 'logging a graphql query' do
|
shared_examples 'logging a graphql query' do
|
||||||
|
|
|
@ -56,6 +56,7 @@ RSpec.shared_context 'project navbar structure' do
|
||||||
nav_item: _('CI / CD'),
|
nav_item: _('CI / CD'),
|
||||||
nav_sub_items: [
|
nav_sub_items: [
|
||||||
_('Pipelines'),
|
_('Pipelines'),
|
||||||
|
s_('Pipelines|Editor'),
|
||||||
_('Jobs'),
|
_('Jobs'),
|
||||||
_('Artifacts'),
|
_('Artifacts'),
|
||||||
_('Schedules')
|
_('Schedules')
|
||||||
|
|
|
@ -219,6 +219,22 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'pipeline editor link' do
|
||||||
|
it 'shows the pipeline editor link' do
|
||||||
|
render
|
||||||
|
|
||||||
|
expect(rendered).to have_link('Editor', href: project_ci_pipeline_editor_path(project))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not show the pipeline editor link' do
|
||||||
|
allow(view).to receive(:can_view_pipeline_editor?).and_return(false)
|
||||||
|
|
||||||
|
render
|
||||||
|
|
||||||
|
expect(rendered).not_to have_link('Editor', href: project_ci_pipeline_editor_path(project))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'operations settings tab' do
|
describe 'operations settings tab' do
|
||||||
describe 'archive projects' do
|
describe 'archive projects' do
|
||||||
before do
|
before do
|
||||||
|
|
Loading…
Reference in New Issue