Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c6c7437861
commit
d3fc3be040
|
|
@ -45,7 +45,13 @@ export default {
|
|||
|
||||
<template v-else>
|
||||
<blob-content-error v-if="viewerError" :viewer-error="viewerError" />
|
||||
<component :is="viewer" v-else ref="contentViewer" :content="content" />
|
||||
<component
|
||||
:is="viewer"
|
||||
v-else
|
||||
ref="contentViewer"
|
||||
:content="content"
|
||||
:type="activeViewer.fileType"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -5,10 +5,43 @@ import { handleLocationHash } from '../../lib/utils/common_utils';
|
|||
import axios from '../../lib/utils/axios_utils';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
const loadRichBlobViewer = type => {
|
||||
switch (type) {
|
||||
case 'balsamiq':
|
||||
return import(/* webpackChunkName: 'balsamiq_viewer' */ '../balsamiq_viewer');
|
||||
case 'notebook':
|
||||
return import(/* webpackChunkName: 'notebook_viewer' */ '../notebook_viewer');
|
||||
case 'openapi':
|
||||
return import(/* webpackChunkName: 'openapi_viewer' */ '../openapi_viewer');
|
||||
case 'pdf':
|
||||
return import(/* webpackChunkName: 'pdf_viewer' */ '../pdf_viewer');
|
||||
case 'sketch':
|
||||
return import(/* webpackChunkName: 'sketch_viewer' */ '../sketch_viewer');
|
||||
case 'stl':
|
||||
return import(/* webpackChunkName: 'stl_viewer' */ '../stl_viewer');
|
||||
default:
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
export const handleBlobRichViewer = (viewer, type) => {
|
||||
if (!viewer || !type) return;
|
||||
|
||||
loadRichBlobViewer(type)
|
||||
.then(module => module?.default(viewer))
|
||||
.catch(error => {
|
||||
Flash(__('Error loading file viewer.'));
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
export default class BlobViewer {
|
||||
constructor() {
|
||||
const viewer = document.querySelector('.blob-viewer[data-type="rich"]');
|
||||
const type = viewer?.dataset?.richType;
|
||||
BlobViewer.initAuxiliaryViewer();
|
||||
BlobViewer.initRichViewer();
|
||||
|
||||
handleBlobRichViewer(viewer, type);
|
||||
|
||||
this.initMainViewers();
|
||||
}
|
||||
|
|
@ -20,42 +53,6 @@ export default class BlobViewer {
|
|||
BlobViewer.loadViewer(auxiliaryViewer);
|
||||
}
|
||||
|
||||
static initRichViewer() {
|
||||
const viewer = document.querySelector('.blob-viewer[data-type="rich"]');
|
||||
if (!viewer || !viewer.dataset.richType) return;
|
||||
|
||||
const initViewer = promise =>
|
||||
promise
|
||||
.then(module => module.default(viewer))
|
||||
.catch(error => {
|
||||
Flash(__('Error loading file viewer.'));
|
||||
throw error;
|
||||
});
|
||||
|
||||
switch (viewer.dataset.richType) {
|
||||
case 'balsamiq':
|
||||
initViewer(import(/* webpackChunkName: 'balsamiq_viewer' */ '../balsamiq_viewer'));
|
||||
break;
|
||||
case 'notebook':
|
||||
initViewer(import(/* webpackChunkName: 'notebook_viewer' */ '../notebook_viewer'));
|
||||
break;
|
||||
case 'openapi':
|
||||
initViewer(import(/* webpackChunkName: 'openapi_viewer' */ '../openapi_viewer'));
|
||||
break;
|
||||
case 'pdf':
|
||||
initViewer(import(/* webpackChunkName: 'pdf_viewer' */ '../pdf_viewer'));
|
||||
break;
|
||||
case 'sketch':
|
||||
initViewer(import(/* webpackChunkName: 'sketch_viewer' */ '../sketch_viewer'));
|
||||
break;
|
||||
case 'stl':
|
||||
initViewer(import(/* webpackChunkName: 'stl_viewer' */ '../stl_viewer'));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
initMainViewers() {
|
||||
this.$fileHolder = $('.file-holder');
|
||||
if (!this.$fileHolder.length) return;
|
||||
|
|
|
|||
|
|
@ -4,5 +4,9 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
<script>
|
||||
import ViewerMixin from './mixins';
|
||||
import { handleBlobRichViewer } from '~/blob/viewer';
|
||||
|
||||
export default {
|
||||
mixins: [ViewerMixin],
|
||||
mounted() {
|
||||
handleBlobRichViewer(this.$refs.content, this.type);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div v-html="content"></div>
|
||||
<div ref="content" v-html="content"></div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,12 @@ export default {
|
|||
<span :style="labelStyle" class="badge color-label">
|
||||
{{ label.title }}
|
||||
</span>
|
||||
<gl-tooltip :target="() => $refs.regularLabelRef" placement="top" boundary="viewport">
|
||||
<gl-tooltip
|
||||
v-if="label.description"
|
||||
:target="() => $refs.regularLabelRef"
|
||||
placement="top"
|
||||
boundary="viewport"
|
||||
>
|
||||
{{ label.description }}
|
||||
</gl-tooltip>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,12 @@ export default {
|
|||
<span :ref="`labelTitleRef`" :style="labelStyle" class="badge color-label label">
|
||||
{{ label.title }}
|
||||
</span>
|
||||
<gl-tooltip :target="() => $refs.labelTitleRef" placement="top" boundary="viewport">
|
||||
<gl-tooltip
|
||||
v-if="label.description"
|
||||
:target="() => $refs.labelTitleRef"
|
||||
placement="top"
|
||||
boundary="viewport"
|
||||
>
|
||||
<span class="font-weight-bold scoped-label-tooltip-title">{{ __('Scoped label') }}</span
|
||||
><br />
|
||||
{{ label.description }}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { GlButton, GlIcon } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlIcon,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['dropdownButtonText']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-button class="labels-select-dropdown-button w-100 text-left">
|
||||
<span class="dropdown-toggle-text">{{ dropdownButtonText }}</span>
|
||||
<gl-icon name="chevron-down" class="pull-right" />
|
||||
</gl-button>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
|
||||
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DropdownContentsLabelsView,
|
||||
DropdownContentsCreateView,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['showDropdownContentsCreateView']),
|
||||
dropdownContentsView() {
|
||||
if (this.showDropdownContentsCreateView) {
|
||||
return 'dropdown-contents-create-view';
|
||||
}
|
||||
return 'dropdown-contents-labels-view';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="labels-select-dropdown-contents w-100 mt-1 mb-3 py-2 rounded-top rounded-bottom position-absolute"
|
||||
>
|
||||
<component :is="dropdownContentsView" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import {
|
||||
GlTooltipDirective,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlFormInput,
|
||||
GlLink,
|
||||
GlLoadingIcon,
|
||||
} from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlFormInput,
|
||||
GlLink,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
labelTitle: '',
|
||||
selectedColor: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['labelsCreateTitle', 'labelCreateInProgress']),
|
||||
disableCreate() {
|
||||
return !this.labelTitle.length || !this.selectedColor.length || this.labelCreateInProgress;
|
||||
},
|
||||
suggestedColors() {
|
||||
const colorsMap = gon.suggested_label_colors;
|
||||
return Object.keys(colorsMap).map(color => ({ [color]: colorsMap[color] }));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleDropdownContents', 'toggleDropdownContentsCreateView', 'createLabel']),
|
||||
getColorCode(color) {
|
||||
return Object.keys(color).pop();
|
||||
},
|
||||
getColorName(color) {
|
||||
return Object.values(color).pop();
|
||||
},
|
||||
handleColorClick(color) {
|
||||
this.selectedColor = this.getColorCode(color);
|
||||
},
|
||||
handleCreateClick() {
|
||||
this.createLabel({
|
||||
title: this.labelTitle,
|
||||
color: this.selectedColor,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="labels-select-contents-create">
|
||||
<div class="dropdown-title d-flex align-items-center pt-0 pb-2">
|
||||
<gl-button
|
||||
:aria-label="__('Go back')"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="dropdown-header-button p-0"
|
||||
@click="toggleDropdownContentsCreateView"
|
||||
>
|
||||
<gl-icon name="arrow-left" />
|
||||
</gl-button>
|
||||
<span class="flex-grow-1">{{ labelsCreateTitle }}</span>
|
||||
<gl-button
|
||||
:aria-label="__('Close')"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="dropdown-header-button p-0"
|
||||
@click="toggleDropdownContents"
|
||||
>
|
||||
<gl-icon name="close" />
|
||||
</gl-button>
|
||||
</div>
|
||||
<div class="dropdown-input">
|
||||
<gl-form-input
|
||||
v-model.trim="labelTitle"
|
||||
:placeholder="__('Name new label')"
|
||||
:autofocus="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="dropdown-content px-2">
|
||||
<div class="suggest-colors suggest-colors-dropdown mt-0 mb-2">
|
||||
<gl-link
|
||||
v-for="(color, index) in suggestedColors"
|
||||
:key="index"
|
||||
v-gl-tooltip:tooltipcontainer
|
||||
:style="{ backgroundColor: getColorCode(color) }"
|
||||
:title="getColorName(color)"
|
||||
@click.prevent="handleColorClick(color)"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-input-container d-flex">
|
||||
<span
|
||||
class="dropdown-label-color-preview position-relative position-relative d-inline-block"
|
||||
:style="{ backgroundColor: selectedColor }"
|
||||
></span>
|
||||
<gl-form-input v-model.trim="selectedColor" :placeholder="__('Use custom color #FF0000')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-actions clearfix pt-2 px-2">
|
||||
<gl-button
|
||||
:disabled="disableCreate"
|
||||
variant="primary"
|
||||
class="pull-left d-flex align-items-center"
|
||||
@click="handleCreateClick"
|
||||
>
|
||||
<gl-loading-icon v-show="labelCreateInProgress" :inline="true" class="mr-1" />
|
||||
{{ __('Create') }}
|
||||
</gl-button>
|
||||
<gl-button class="pull-right" @click="toggleDropdownContentsCreateView">
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import { GlLoadingIcon, GlButton, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
|
||||
|
||||
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLoadingIcon,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlSearchBoxByType,
|
||||
GlLink,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchKey: '',
|
||||
currentHighlightItem: -1,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'labelsManagePath',
|
||||
'labels',
|
||||
'labelsFetchInProgress',
|
||||
'labelsListTitle',
|
||||
'footerCreateLabelTitle',
|
||||
'footerManageLabelTitle',
|
||||
]),
|
||||
...mapGetters(['selectedLabelsList']),
|
||||
visibleLabels() {
|
||||
if (this.searchKey) {
|
||||
return this.labels.filter(label =>
|
||||
label.title.toLowerCase().includes(this.searchKey.toLowerCase()),
|
||||
);
|
||||
}
|
||||
return this.labels;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchKey(value) {
|
||||
// When there is search string present
|
||||
// and there are matching results,
|
||||
// highlight first item by default.
|
||||
if (value && this.visibleLabels.length) {
|
||||
this.currentHighlightItem = 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchLabels();
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'toggleDropdownContents',
|
||||
'toggleDropdownContentsCreateView',
|
||||
'fetchLabels',
|
||||
'updateSelectedLabels',
|
||||
]),
|
||||
getDropdownLabelBoxStyle(label) {
|
||||
return {
|
||||
backgroundColor: label.color,
|
||||
};
|
||||
},
|
||||
isLabelSelected(label) {
|
||||
return this.selectedLabelsList.includes(label.id);
|
||||
},
|
||||
/**
|
||||
* This method scrolls item from dropdown into
|
||||
* the view if it is off the viewable area of the
|
||||
* container.
|
||||
*/
|
||||
scrollIntoViewIfNeeded() {
|
||||
const highlightedLabel = this.$refs.labelsListContainer.querySelector('.is-focused');
|
||||
|
||||
if (highlightedLabel) {
|
||||
const rect = highlightedLabel.getBoundingClientRect();
|
||||
if (rect.bottom > this.$refs.labelsListContainer.clientHeight) {
|
||||
highlightedLabel.scrollIntoView(false);
|
||||
}
|
||||
if (rect.top < 0) {
|
||||
highlightedLabel.scrollIntoView();
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* This method enables keyboard navigation support for
|
||||
* the dropdown.
|
||||
*/
|
||||
handleKeyDown(e) {
|
||||
if (e.keyCode === UP_KEY_CODE && this.currentHighlightItem > 0) {
|
||||
this.currentHighlightItem -= 1;
|
||||
} else if (
|
||||
e.keyCode === DOWN_KEY_CODE &&
|
||||
this.currentHighlightItem < this.visibleLabels.length - 1
|
||||
) {
|
||||
this.currentHighlightItem += 1;
|
||||
} else if (e.keyCode === ENTER_KEY_CODE && this.currentHighlightItem > -1) {
|
||||
this.updateSelectedLabels([this.visibleLabels[this.currentHighlightItem]]);
|
||||
} else if (e.keyCode === ESC_KEY_CODE) {
|
||||
this.toggleDropdownContents();
|
||||
}
|
||||
|
||||
if (e.keyCode !== ESC_KEY_CODE) {
|
||||
// Scroll the list only after highlighting
|
||||
// styles are rendered completely.
|
||||
this.$nextTick(() => {
|
||||
this.scrollIntoViewIfNeeded();
|
||||
});
|
||||
}
|
||||
},
|
||||
handleLabelClick(label) {
|
||||
this.updateSelectedLabels([label]);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="labels-select-contents-list" @keydown="handleKeyDown">
|
||||
<gl-loading-icon
|
||||
v-if="labelsFetchInProgress"
|
||||
class="labels-fetch-loading position-absolute d-flex align-items-center w-100 h-100"
|
||||
size="md"
|
||||
/>
|
||||
<div class="dropdown-title d-flex align-items-center pt-0 pb-2">
|
||||
<span class="flex-grow-1">{{ labelsListTitle }}</span>
|
||||
<gl-button
|
||||
:aria-label="__('Close')"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="dropdown-header-button p-0"
|
||||
@click="toggleDropdownContents"
|
||||
>
|
||||
<gl-icon name="close" />
|
||||
</gl-button>
|
||||
</div>
|
||||
<div class="dropdown-input">
|
||||
<gl-search-box-by-type v-model="searchKey" :autofocus="true" />
|
||||
</div>
|
||||
<div v-if="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content">
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li v-for="(label, index) in visibleLabels" :key="label.id" class="d-block text-left">
|
||||
<gl-link
|
||||
class="d-flex align-items-baseline text-break-word label-item"
|
||||
:class="{ 'is-focused': index === currentHighlightItem }"
|
||||
@click="handleLabelClick(label)"
|
||||
>
|
||||
<gl-icon v-show="label.set" name="mobile-issue-close" class="mr-2 align-self-center" />
|
||||
<span v-show="!label.set" class="mr-3 pr-2"></span>
|
||||
<span class="dropdown-label-box" :style="getDropdownLabelBoxStyle(label)"></span>
|
||||
<span>{{ label.title }}</span>
|
||||
</gl-link>
|
||||
</li>
|
||||
<li v-if="!visibleLabels.length" class="p-2 text-center">
|
||||
{{ __('No matching results') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown-footer">
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<gl-button
|
||||
variant="link"
|
||||
class="d-flex w-100 flex-row text-break-word label-item"
|
||||
@click="toggleDropdownContentsCreateView"
|
||||
>{{ footerCreateLabelTitle }}</gl-button
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">
|
||||
{{ footerManageLabelTitle }}
|
||||
</gl-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
labelsSelectInProgress: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['allowLabelEdit', 'labelsFetchInProgress']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleDropdownContents']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="title hide-collapsed append-bottom-10">
|
||||
{{ __('Labels') }}
|
||||
<template v-if="allowLabelEdit">
|
||||
<gl-loading-icon v-show="labelsSelectInProgress" inline />
|
||||
<gl-button
|
||||
variant="link"
|
||||
class="pull-right js-sidebar-dropdown-toggle"
|
||||
data-qa-selector="labels_edit_button"
|
||||
@click="toggleDropdownContents"
|
||||
>{{ __('Edit') }}</gl-button
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { GlLabel } from '@gitlab/ui';
|
||||
|
||||
import { isScopedLabel } from '~/lib/utils/common_utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLabel,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'selectedLabels',
|
||||
'allowScopedLabels',
|
||||
'labelsFilterBasePath',
|
||||
'scopedLabelsDocumentationPath',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
labelFilterUrl(label) {
|
||||
return `${this.labelsFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`;
|
||||
},
|
||||
scopedLabel(label) {
|
||||
return this.allowScopedLabels && isScopedLabel(label);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'has-labels': selectedLabels.length,
|
||||
}"
|
||||
class="hide-collapsed value issuable-show-labels js-value"
|
||||
>
|
||||
<span v-if="!selectedLabels.length" class="text-secondary">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<template v-for="label in selectedLabels" v-else>
|
||||
<gl-label
|
||||
:key="label.id"
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
:background-color="label.color"
|
||||
:target="labelFilterUrl(label)"
|
||||
:scoped="scopedLabel(label)"
|
||||
:scoped-labels-documentation-link="scopedLabelsDocumentationPath"
|
||||
tooltip-placement="top"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
<script>
|
||||
import Vue from 'vue';
|
||||
import Vuex, { mapState, mapActions } from 'vuex';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
|
||||
|
||||
import labelsSelectModule from './store';
|
||||
|
||||
import DropdownTitle from './dropdown_title.vue';
|
||||
import DropdownValue from './dropdown_value.vue';
|
||||
import DropdownButton from './dropdown_button.vue';
|
||||
import DropdownContents from './dropdown_contents.vue';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default {
|
||||
store: new Vuex.Store(labelsSelectModule()),
|
||||
components: {
|
||||
DropdownTitle,
|
||||
DropdownValue,
|
||||
DropdownButton,
|
||||
DropdownContents,
|
||||
DropdownValueCollapsed,
|
||||
},
|
||||
props: {
|
||||
allowLabelEdit: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
allowLabelCreate: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
allowScopedLabels: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
dropdownOnly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
selectedLabels: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
labelsSelectInProgress: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
labelsFetchPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
labelsManagePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
labelsFilterBasePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
scopedLabelsDocumentationPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
labelsListTitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('Assign labels'),
|
||||
},
|
||||
labelsCreateTitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('Create group label'),
|
||||
},
|
||||
footerCreateLabelTitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('Create group label'),
|
||||
},
|
||||
footerManageLabelTitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('Manage group labels'),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['showDropdownButton', 'showDropdownContents']),
|
||||
},
|
||||
watch: {
|
||||
selectedLabels(selectedLabels) {
|
||||
this.setInitialState({
|
||||
selectedLabels,
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setInitialState({
|
||||
dropdownOnly: this.dropdownOnly,
|
||||
allowLabelEdit: this.allowLabelEdit,
|
||||
allowLabelCreate: this.allowLabelCreate,
|
||||
allowScopedLabels: this.allowScopedLabels,
|
||||
selectedLabels: this.selectedLabels,
|
||||
labelsFetchPath: this.labelsFetchPath,
|
||||
labelsManagePath: this.labelsManagePath,
|
||||
labelsFilterBasePath: this.labelsFilterBasePath,
|
||||
scopedLabelsDocumentationPath: this.scopedLabelsDocumentationPath,
|
||||
labelsListTitle: this.labelsListTitle,
|
||||
labelsCreateTitle: this.labelsCreateTitle,
|
||||
footerCreateLabelTitle: this.footerCreateLabelTitle,
|
||||
footerManageLabelTitle: this.footerManageLabelTitle,
|
||||
});
|
||||
|
||||
this.$store.subscribeAction({
|
||||
after: this.handleVuexActionDispatch,
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setInitialState']),
|
||||
/**
|
||||
* This method differentiates between
|
||||
* dispatched actions and calls necessary method.
|
||||
*/
|
||||
handleVuexActionDispatch(action, state) {
|
||||
if (
|
||||
action.type === 'toggleDropdownContents' &&
|
||||
!state.showDropdownButton &&
|
||||
!state.showDropdownContents
|
||||
) {
|
||||
this.handleDropdownClose(state.labels.filter(label => label.touched));
|
||||
}
|
||||
},
|
||||
handleDropdownClose(labels) {
|
||||
// Only emit label updates if there are any labels to update
|
||||
// on UI.
|
||||
if (labels.length) this.$emit('updateSelectedLabels', labels);
|
||||
this.$emit('onDropdownClose');
|
||||
},
|
||||
handleCollapsedValueClick() {
|
||||
this.$emit('toggleCollapse');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="labels-select-wrapper position-relative">
|
||||
<div v-if="!dropdownOnly">
|
||||
<dropdown-value-collapsed
|
||||
v-if="allowLabelCreate"
|
||||
:labels="selectedLabels"
|
||||
@onValueClick="handleCollapsedValueClick"
|
||||
/>
|
||||
<dropdown-title
|
||||
:allow-label-edit="allowLabelEdit"
|
||||
:labels-select-in-progress="labelsSelectInProgress"
|
||||
/>
|
||||
<dropdown-value v-show="!showDropdownButton">
|
||||
<slot></slot>
|
||||
</dropdown-value>
|
||||
<dropdown-button v-show="showDropdownButton" />
|
||||
<dropdown-contents v-if="showDropdownButton && showDropdownContents" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import flash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export const setInitialState = ({ commit }, props) => commit(types.SET_INITIAL_STATE, props);
|
||||
|
||||
export const toggleDropdownButton = ({ commit }) => commit(types.TOGGLE_DROPDOWN_BUTTON);
|
||||
export const toggleDropdownContents = ({ commit }) => commit(types.TOGGLE_DROPDOWN_CONTENTS);
|
||||
|
||||
export const toggleDropdownContentsCreateView = ({ commit }) =>
|
||||
commit(types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW);
|
||||
|
||||
export const requestLabels = ({ commit }) => commit(types.REQUEST_LABELS);
|
||||
export const receiveLabelsSuccess = ({ commit }, labels) =>
|
||||
commit(types.RECEIVE_SET_LABELS_SUCCESS, labels);
|
||||
export const receiveLabelsFailure = ({ commit }) => {
|
||||
commit(types.RECEIVE_SET_LABELS_FAILURE);
|
||||
flash(__('Error fetching labels.'));
|
||||
};
|
||||
export const fetchLabels = ({ state, dispatch }) => {
|
||||
dispatch('requestLabels');
|
||||
axios
|
||||
.get(state.labelsFetchPath)
|
||||
.then(({ data }) => {
|
||||
dispatch('receiveLabelsSuccess', data);
|
||||
})
|
||||
.catch(() => dispatch('receiveLabelsFailure'));
|
||||
};
|
||||
|
||||
export const requestCreateLabel = ({ commit }) => commit(types.REQUEST_CREATE_LABEL);
|
||||
export const receiveCreateLabelSuccess = ({ commit }) => commit(types.RECEIVE_CREATE_LABEL_SUCCESS);
|
||||
export const receiveCreateLabelFailure = ({ commit }) => {
|
||||
commit(types.RECEIVE_CREATE_LABEL_FAILURE);
|
||||
flash(__('Error creating label.'));
|
||||
};
|
||||
export const createLabel = ({ state, dispatch }, label) => {
|
||||
dispatch('requestCreateLabel');
|
||||
axios
|
||||
.post(state.labelsManagePath, {
|
||||
label,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.id) {
|
||||
dispatch('receiveCreateLabelSuccess');
|
||||
dispatch('toggleDropdownContentsCreateView');
|
||||
} else {
|
||||
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
|
||||
throw new Error('Error Creating Label');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch('receiveCreateLabelFailure');
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSelectedLabels = ({ commit }, labels) =>
|
||||
commit(types.UPDATE_SELECTED_LABELS, { labels });
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { __, s__, sprintf } from '~/locale';
|
||||
|
||||
/**
|
||||
* Returns string representing current labels
|
||||
* selection on dropdown button.
|
||||
*
|
||||
* @param {object} state
|
||||
*/
|
||||
export const dropdownButtonText = state => {
|
||||
const selectedLabels = state.labels.filter(label => label.set);
|
||||
if (!selectedLabels.length) {
|
||||
return __('Label');
|
||||
} else if (selectedLabels.length > 1) {
|
||||
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
|
||||
firstLabelName: selectedLabels[0].title,
|
||||
remainingLabelCount: selectedLabels.length - 1,
|
||||
});
|
||||
}
|
||||
return selectedLabels[0].title;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns array containing only label IDs from
|
||||
* selectedLabels array.
|
||||
* @param {object} state
|
||||
*/
|
||||
export const selectedLabelsList = state => state.selectedLabels.map(label => label.id);
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import * as actions from './actions';
|
||||
import * as getters from './getters';
|
||||
import mutations from './mutations';
|
||||
import state from './state';
|
||||
|
||||
export default () => ({
|
||||
namespaced: true,
|
||||
state: state(),
|
||||
actions,
|
||||
getters,
|
||||
mutations,
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
|
||||
|
||||
export const REQUEST_LABELS = 'REQUEST_LABELS';
|
||||
export const RECEIVE_LABELS_SUCCESS = 'RECEIVE_LABELS_SUCCESS';
|
||||
export const RECEIVE_LABELS_FAILURE = 'RECEIVE_LABELS_FAILURE';
|
||||
|
||||
export const REQUEST_SET_LABELS = 'REQUEST_SET_LABELS';
|
||||
export const RECEIVE_SET_LABELS_SUCCESS = 'RECEIVE_SET_LABELS_SUCCESS';
|
||||
export const RECEIVE_SET_LABELS_FAILURE = 'RECEIVE_SET_LABELS_FAILURE';
|
||||
|
||||
export const REQUEST_CREATE_LABEL = 'REQUEST_CREATE_LABEL';
|
||||
export const RECEIVE_CREATE_LABEL_SUCCESS = 'RECEIVE_CREATE_LABEL_SUCCESS';
|
||||
export const RECEIVE_CREATE_LABEL_FAILURE = 'RECEIVE_CREATE_LABEL_FAILURE';
|
||||
|
||||
export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY';
|
||||
export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS';
|
||||
|
||||
export const UPDATE_SELECTED_LABELS = 'UPDATE_SELECTED_LABELS';
|
||||
|
||||
export const TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW = 'TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW';
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_INITIAL_STATE](state, props) {
|
||||
Object.assign(state, { ...props });
|
||||
},
|
||||
|
||||
[types.TOGGLE_DROPDOWN_BUTTON](state) {
|
||||
state.showDropdownButton = !state.showDropdownButton;
|
||||
},
|
||||
|
||||
[types.TOGGLE_DROPDOWN_CONTENTS](state) {
|
||||
if (!state.dropdownOnly) {
|
||||
state.showDropdownButton = !state.showDropdownButton;
|
||||
}
|
||||
state.showDropdownContents = !state.showDropdownContents;
|
||||
// Ensure that Create View is hidden by default
|
||||
// when dropdown contents are revealed.
|
||||
if (state.showDropdownContents) {
|
||||
state.showDropdownContentsCreateView = false;
|
||||
}
|
||||
},
|
||||
|
||||
[types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW](state) {
|
||||
state.showDropdownContentsCreateView = !state.showDropdownContentsCreateView;
|
||||
},
|
||||
|
||||
[types.REQUEST_LABELS](state) {
|
||||
state.labelsFetchInProgress = true;
|
||||
},
|
||||
[types.RECEIVE_SET_LABELS_SUCCESS](state, labels) {
|
||||
// Iterate over every label and add a `set` prop
|
||||
// to determine whether it is already a part of
|
||||
// selectedLabels array.
|
||||
const selectedLabelIds = state.selectedLabels.map(label => label.id);
|
||||
state.labelsFetchInProgress = false;
|
||||
state.labels = labels.reduce((allLabels, label) => {
|
||||
allLabels.push({
|
||||
...label,
|
||||
set: selectedLabelIds.includes(label.id),
|
||||
});
|
||||
return allLabels;
|
||||
}, []);
|
||||
},
|
||||
[types.RECEIVE_SET_LABELS_FAILURE](state) {
|
||||
state.labelsFetchInProgress = false;
|
||||
},
|
||||
|
||||
[types.REQUEST_CREATE_LABEL](state) {
|
||||
state.labelCreateInProgress = true;
|
||||
},
|
||||
[types.RECEIVE_CREATE_LABEL_SUCCESS](state) {
|
||||
state.labelCreateInProgress = false;
|
||||
},
|
||||
[types.RECEIVE_CREATE_LABEL_FAILURE](state) {
|
||||
state.labelCreateInProgress = false;
|
||||
},
|
||||
|
||||
[types.UPDATE_SELECTED_LABELS](state, { labels }) {
|
||||
// Iterate over all the labels and update
|
||||
// `set` prop value to represent their current state.
|
||||
const labelIds = labels.map(label => label.id);
|
||||
state.labels = state.labels.reduce((allLabels, label) => {
|
||||
if (labelIds.includes(label.id)) {
|
||||
allLabels.push({
|
||||
...label,
|
||||
touched: true,
|
||||
set: !label.set,
|
||||
});
|
||||
} else {
|
||||
allLabels.push(label);
|
||||
}
|
||||
return allLabels;
|
||||
}, []);
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
export default () => ({
|
||||
// Initial Data
|
||||
labels: [],
|
||||
selectedLabels: [],
|
||||
labelsListTitle: '',
|
||||
labelsCreateTitle: '',
|
||||
footerCreateLabelTitle: '',
|
||||
footerManageLabelTitle: '',
|
||||
|
||||
// Paths
|
||||
namespace: '',
|
||||
labelsFetchPath: '',
|
||||
labelsFilterBasePath: '',
|
||||
scopedLabelsDocumentationPath: '#',
|
||||
|
||||
// UI Flags
|
||||
allowLabelCreate: false,
|
||||
allowLabelEdit: false,
|
||||
allowScopedLabels: false,
|
||||
dropdownOnly: false,
|
||||
showDropdownButton: false,
|
||||
showDropdownContents: false,
|
||||
showDropdownContentsCreateView: false,
|
||||
labelsFetchInProgress: false,
|
||||
labelCreateInProgress: false,
|
||||
selectedLabelsUpdated: false,
|
||||
});
|
||||
|
|
@ -1019,3 +1019,54 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
|
|||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.labels-select-wrapper {
|
||||
.labels-select-dropdown-contents {
|
||||
min-height: $dropdown-min-height;
|
||||
max-height: 330px;
|
||||
background-color: $white-light;
|
||||
border: 1px solid $border-color;
|
||||
box-shadow: 0 2px 4px $dropdown-shadow-color;
|
||||
z-index: 2;
|
||||
|
||||
.dropdown-content {
|
||||
height: 135px;
|
||||
}
|
||||
}
|
||||
|
||||
.labels-fetch-loading {
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0.5;
|
||||
background-color: $white-light;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dropdown-header-button {
|
||||
.gl-icon {
|
||||
color: $dropdown-title-btn-color;
|
||||
|
||||
&:hover {
|
||||
color: $gl-gray-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 8px 20px;
|
||||
|
||||
&:hover,
|
||||
&.is-focused {
|
||||
@include dropdown-item-hover;
|
||||
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.color-input-container {
|
||||
.dropdown-label-color-preview {
|
||||
border: 1px solid $gray-200;
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -281,11 +281,10 @@ class Snippet < ApplicationRecord
|
|||
end
|
||||
|
||||
def create_repository
|
||||
return if repository_exists?
|
||||
return if repository_exists? && snippet_repository
|
||||
|
||||
repository.create_if_not_exists
|
||||
|
||||
track_snippet_repository if repository_exists?
|
||||
track_snippet_repository
|
||||
end
|
||||
|
||||
def track_snippet_repository
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ module Snippets
|
|||
class UpdateService < Snippets::BaseService
|
||||
include SpamCheckMethods
|
||||
|
||||
UpdateError = Class.new(StandardError)
|
||||
CreateRepositoryError = Class.new(StandardError)
|
||||
|
||||
def execute(snippet)
|
||||
# check that user is allowed to set specified visibility_level
|
||||
new_visibility = visibility_level
|
||||
|
|
@ -20,11 +23,7 @@ module Snippets
|
|||
snippet.assign_attributes(params)
|
||||
spam_check(snippet, current_user)
|
||||
|
||||
snippet_saved = snippet.with_transaction_returning_status do
|
||||
snippet.save
|
||||
end
|
||||
|
||||
if snippet_saved
|
||||
if save_and_commit(snippet)
|
||||
Gitlab::UsageDataCounters::SnippetCounter.count(:update)
|
||||
|
||||
ServiceResponse.success(payload: { snippet: snippet } )
|
||||
|
|
@ -32,5 +31,54 @@ module Snippets
|
|||
snippet_error_response(snippet, 400)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def save_and_commit(snippet)
|
||||
snippet.with_transaction_returning_status do
|
||||
snippet.save.tap do |saved|
|
||||
break false unless saved
|
||||
|
||||
# In order to avoid non migrated snippets scenarios,
|
||||
# if the snippet does not have a repository we created it
|
||||
# We don't need to check if the repository exists
|
||||
# because `create_repository` already handles it
|
||||
if Feature.enabled?(:version_snippets, current_user)
|
||||
create_repository_for(snippet)
|
||||
end
|
||||
|
||||
# If the snippet repository exists we commit always
|
||||
# the changes
|
||||
create_commit(snippet) if snippet.repository_exists?
|
||||
end
|
||||
rescue
|
||||
snippet.errors.add(:base, 'Error updating the snippet')
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def create_repository_for(snippet)
|
||||
snippet.create_repository
|
||||
|
||||
raise CreateRepositoryError, 'Repository could not be created' unless snippet.repository_exists?
|
||||
end
|
||||
|
||||
def create_commit(snippet)
|
||||
raise UpdateError unless snippet.snippet_repository
|
||||
|
||||
commit_attrs = {
|
||||
branch_name: 'master',
|
||||
message: 'Update snippet'
|
||||
}
|
||||
|
||||
snippet.snippet_repository.multi_files_action(current_user, snippet_files(snippet), commit_attrs)
|
||||
end
|
||||
|
||||
def snippet_files(snippet)
|
||||
[{ previous_path: snippet.blobs.first&.path,
|
||||
file_path: params[:file_name],
|
||||
content: params[:content] }]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Special handling for the rich viewer on specific file types
|
||||
merge_request: 26260
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update files when snippet is updated
|
||||
merge_request: 23993
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Make design_management_versions.created_at not null
|
||||
merge_request: 20182
|
||||
author: Lee Tickett
|
||||
type: other
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MakeCreatedAtNotNullInDesignManagementVersions < ActiveRecord::Migration[5.2]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
change_column_null :design_management_versions, :created_at, false, Time.now.to_s(:db)
|
||||
end
|
||||
|
||||
def down
|
||||
change_column_null :design_management_versions, :created_at, true
|
||||
end
|
||||
end
|
||||
|
|
@ -1446,7 +1446,7 @@ ActiveRecord::Schema.define(version: 2020_03_03_074328) do
|
|||
create_table "design_management_versions", force: :cascade do |t|
|
||||
t.binary "sha", null: false
|
||||
t.bigint "issue_id"
|
||||
t.datetime_with_timezone "created_at"
|
||||
t.datetime_with_timezone "created_at", null: false
|
||||
t.integer "author_id"
|
||||
t.index ["author_id"], name: "index_design_management_versions_on_author_id", where: "(author_id IS NOT NULL)"
|
||||
t.index ["issue_id"], name: "index_design_management_versions_on_issue_id"
|
||||
|
|
|
|||
|
|
@ -71,12 +71,6 @@ the need as part of the product in a future version of GitLab!
|
|||
Implement each task as an isolated piece of functionality and place it in its
|
||||
own directory under `danger` as `danger/<task-name>/Dangerfile`.
|
||||
|
||||
Add a line to the top-level `Dangerfile` to ensure it is loaded like:
|
||||
|
||||
```ruby
|
||||
danger.import_dangerfile('danger/<task-name>')
|
||||
```
|
||||
|
||||
Each task should be isolated from the others, and able to function in isolation.
|
||||
If there is code that should be shared between multiple tasks, add a plugin to
|
||||
`danger/plugins/...` and require it in each task that needs it. You can also
|
||||
|
|
|
|||
|
|
@ -7778,6 +7778,9 @@ msgstr ""
|
|||
msgid "Error creating epic"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error creating label."
|
||||
msgstr ""
|
||||
|
||||
msgid "Error deleting %{issuableType}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -21315,6 +21318,9 @@ msgstr ""
|
|||
msgid "Use an one time password authenticator on your mobile device or computer to enable two-factor authentication (2FA)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Use custom color #FF0000"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use group milestones to manage issues from multiple projects in the same milestone."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import RichViewer from '~/vue_shared/components/blob_viewers/rich_viewer.vue';
|
||||
import { handleBlobRichViewer } from '~/blob/viewer';
|
||||
|
||||
jest.mock('~/blob/viewer');
|
||||
|
||||
describe('Blob Rich Viewer component', () => {
|
||||
let wrapper;
|
||||
const content = '<h1 id="markdown">Foo Bar</h1>';
|
||||
const defaultType = 'markdown';
|
||||
|
||||
function createComponent() {
|
||||
function createComponent(type = defaultType) {
|
||||
wrapper = shallowMount(RichViewer, {
|
||||
propsData: {
|
||||
content,
|
||||
type,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -24,4 +29,8 @@ describe('Blob Rich Viewer component', () => {
|
|||
it('renders the passed content without transformations', () => {
|
||||
expect(wrapper.html()).toContain(content);
|
||||
});
|
||||
|
||||
it('queries for advanced viewer', () => {
|
||||
expect(handleBlobRichViewer).toHaveBeenCalledWith(expect.anything(), defaultType);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ describe('Blob Simple Viewer component', () => {
|
|||
wrapper = shallowMount(SimpleViewer, {
|
||||
propsData: {
|
||||
content,
|
||||
type: 'text',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
|
||||
|
||||
import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
|
||||
|
||||
import { mockConfig } from './mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const createComponent = (initialState = mockConfig) => {
|
||||
const store = new Vuex.Store(labelSelectModule());
|
||||
|
||||
store.dispatch('setInitialState', initialState);
|
||||
|
||||
return shallowMount(DropdownButton, {
|
||||
localVue,
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
||||
describe('DropdownButton', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('renders component container element', () => {
|
||||
expect(wrapper.is('gl-button-stub')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders button text element', () => {
|
||||
const dropdownTextEl = wrapper.find('.dropdown-toggle-text');
|
||||
|
||||
expect(dropdownTextEl.exists()).toBe(true);
|
||||
expect(dropdownTextEl.text()).toBe('Label');
|
||||
});
|
||||
|
||||
it('renders chevron icon element', () => {
|
||||
const iconEl = wrapper.find(GlIcon);
|
||||
|
||||
expect(iconEl.exists()).toBe(true);
|
||||
expect(iconEl.props('name')).toBe('chevron-down');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import { GlButton, GlIcon, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
|
||||
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue';
|
||||
|
||||
import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
|
||||
|
||||
import { mockConfig, mockSuggestedColors } from './mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const createComponent = (initialState = mockConfig) => {
|
||||
const store = new Vuex.Store(labelSelectModule());
|
||||
|
||||
store.dispatch('setInitialState', initialState);
|
||||
|
||||
return shallowMount(DropdownContentsCreateView, {
|
||||
localVue,
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
||||
describe('DropdownContentsCreateView', () => {
|
||||
let wrapper;
|
||||
const colors = Object.keys(mockSuggestedColors).map(color => ({
|
||||
[color]: mockSuggestedColors[color],
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
gon.suggested_label_colors = mockSuggestedColors;
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('computed', () => {
|
||||
describe('disableCreate', () => {
|
||||
it('returns `true` when label title and color is not defined', () => {
|
||||
expect(wrapper.vm.disableCreate).toBe(true);
|
||||
});
|
||||
|
||||
it('returns `true` when `labelCreateInProgress` is true', () => {
|
||||
wrapper.setData({
|
||||
labelTitle: 'Foo',
|
||||
selectedColor: '#ff0000',
|
||||
});
|
||||
wrapper.vm.$store.dispatch('requestCreateLabel');
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.vm.disableCreate).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns `false` when label title and color is defined and create request is not already in progress', () => {
|
||||
wrapper.setData({
|
||||
labelTitle: 'Foo',
|
||||
selectedColor: '#ff0000',
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.vm.disableCreate).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('suggestedColors', () => {
|
||||
it('returns array of color objects containing color code and name', () => {
|
||||
colors.forEach((color, index) => {
|
||||
expect(wrapper.vm.suggestedColors[index]).toEqual(expect.objectContaining(color));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('getColorCode', () => {
|
||||
it('returns color code from color object', () => {
|
||||
expect(wrapper.vm.getColorCode(colors[0])).toBe(Object.keys(colors[0]).pop());
|
||||
});
|
||||
});
|
||||
|
||||
describe('getColorName', () => {
|
||||
it('returns color name from color object', () => {
|
||||
expect(wrapper.vm.getColorName(colors[0])).toBe(Object.values(colors[0]).pop());
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleColorClick', () => {
|
||||
it('sets provided `color` param to `selectedColor` prop', () => {
|
||||
wrapper.vm.handleColorClick(colors[0]);
|
||||
|
||||
expect(wrapper.vm.selectedColor).toBe(Object.keys(colors[0]).pop());
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCreateClick', () => {
|
||||
it('calls action `createLabel` with object containing `labelTitle` & `selectedColor`', () => {
|
||||
jest.spyOn(wrapper.vm, 'createLabel').mockImplementation();
|
||||
wrapper.setData({
|
||||
labelTitle: 'Foo',
|
||||
selectedColor: '#ff0000',
|
||||
});
|
||||
|
||||
wrapper.vm.handleCreateClick();
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.vm.createLabel).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: 'Foo',
|
||||
color: '#ff0000',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('renders component container element with class "labels-select-contents-create"', () => {
|
||||
expect(wrapper.attributes('class')).toContain('labels-select-contents-create');
|
||||
});
|
||||
|
||||
it('renders dropdown back button element', () => {
|
||||
const backBtnEl = wrapper
|
||||
.find('.dropdown-title')
|
||||
.findAll(GlButton)
|
||||
.at(0);
|
||||
|
||||
expect(backBtnEl.exists()).toBe(true);
|
||||
expect(backBtnEl.attributes('aria-label')).toBe('Go back');
|
||||
expect(backBtnEl.find(GlIcon).props('name')).toBe('arrow-left');
|
||||
});
|
||||
|
||||
it('renders dropdown title element', () => {
|
||||
const headerEl = wrapper.find('.dropdown-title > span');
|
||||
|
||||
expect(headerEl.exists()).toBe(true);
|
||||
expect(headerEl.text()).toBe('Create label');
|
||||
});
|
||||
|
||||
it('renders dropdown close button element', () => {
|
||||
const closeBtnEl = wrapper
|
||||
.find('.dropdown-title')
|
||||
.findAll(GlButton)
|
||||
.at(1);
|
||||
|
||||
expect(closeBtnEl.exists()).toBe(true);
|
||||
expect(closeBtnEl.attributes('aria-label')).toBe('Close');
|
||||
expect(closeBtnEl.find(GlIcon).props('name')).toBe('close');
|
||||
});
|
||||
|
||||
it('renders label title input element', () => {
|
||||
const titleInputEl = wrapper.find('.dropdown-input').find(GlFormInput);
|
||||
|
||||
expect(titleInputEl.exists()).toBe(true);
|
||||
expect(titleInputEl.attributes('placeholder')).toBe('Name new label');
|
||||
expect(titleInputEl.attributes('autofocus')).toBe('true');
|
||||
});
|
||||
|
||||
it('renders color block element for all suggested colors', () => {
|
||||
const colorBlocksEl = wrapper.find('.dropdown-content').findAll(GlLink);
|
||||
|
||||
colorBlocksEl.wrappers.forEach((colorBlock, index) => {
|
||||
expect(colorBlock.attributes('style')).toContain('background-color');
|
||||
expect(colorBlock.attributes('title')).toBe(Object.values(colors[index]).pop());
|
||||
});
|
||||
});
|
||||
|
||||
it('renders color input element', () => {
|
||||
wrapper.setData({
|
||||
selectedColor: '#ff0000',
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
const colorPreviewEl = wrapper.find(
|
||||
'.color-input-container > .dropdown-label-color-preview',
|
||||
);
|
||||
const colorInputEl = wrapper.find('.color-input-container').find(GlFormInput);
|
||||
|
||||
expect(colorPreviewEl.exists()).toBe(true);
|
||||
expect(colorPreviewEl.attributes('style')).toContain('background-color');
|
||||
expect(colorInputEl.exists()).toBe(true);
|
||||
expect(colorInputEl.attributes('placeholder')).toBe('Use custom color #FF0000');
|
||||
expect(colorInputEl.attributes('value')).toBe('#ff0000');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders create button element', () => {
|
||||
const createBtnEl = wrapper
|
||||
.find('.dropdown-actions')
|
||||
.findAll(GlButton)
|
||||
.at(0);
|
||||
|
||||
expect(createBtnEl.exists()).toBe(true);
|
||||
expect(createBtnEl.text()).toContain('Create');
|
||||
});
|
||||
|
||||
it('shows gl-loading-icon within create button element when `labelCreateInProgress` is `true`', () => {
|
||||
wrapper.vm.$store.dispatch('requestCreateLabel');
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
const loadingIconEl = wrapper.find('.dropdown-actions').find(GlLoadingIcon);
|
||||
|
||||
expect(loadingIconEl.exists()).toBe(true);
|
||||
expect(loadingIconEl.isVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders cancel button element', () => {
|
||||
const cancelBtnEl = wrapper
|
||||
.find('.dropdown-actions')
|
||||
.findAll(GlButton)
|
||||
.at(1);
|
||||
|
||||
expect(cancelBtnEl.exists()).toBe(true);
|
||||
expect(cancelBtnEl.text()).toContain('Cancel');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import { GlButton, GlLoadingIcon, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
|
||||
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
|
||||
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue';
|
||||
|
||||
import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
|
||||
import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
|
||||
import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
|
||||
import * as getters from '~/vue_shared/components/sidebar/labels_select_vue/store/getters';
|
||||
|
||||
import { mockConfig, mockLabels, mockRegularLabel } from './mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const createComponent = (initialState = mockConfig) => {
|
||||
const store = new Vuex.Store({
|
||||
getters,
|
||||
mutations,
|
||||
state: {
|
||||
...defaultState(),
|
||||
footerCreateLabelTitle: 'Create label',
|
||||
footerManageLabelTitle: 'Manage labels',
|
||||
},
|
||||
actions: {
|
||||
...actions,
|
||||
fetchLabels: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
store.dispatch('setInitialState', initialState);
|
||||
store.dispatch('receiveLabelsSuccess', mockLabels);
|
||||
|
||||
return shallowMount(DropdownContentsLabelsView, {
|
||||
localVue,
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
||||
describe('DropdownContentsLabelsView', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('computed', () => {
|
||||
describe('visibleLabels', () => {
|
||||
it('returns matching labels filtered with `searchKey`', () => {
|
||||
wrapper.setData({
|
||||
searchKey: 'bug',
|
||||
});
|
||||
|
||||
expect(wrapper.vm.visibleLabels.length).toBe(1);
|
||||
expect(wrapper.vm.visibleLabels[0].title).toBe('Bug');
|
||||
});
|
||||
|
||||
it('returns all labels when `searchKey` is empty', () => {
|
||||
wrapper.setData({
|
||||
searchKey: '',
|
||||
});
|
||||
|
||||
expect(wrapper.vm.visibleLabels.length).toBe(mockLabels.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('getDropdownLabelBoxStyle', () => {
|
||||
it('returns an object containing `backgroundColor` based on provided `label` param', () => {
|
||||
expect(wrapper.vm.getDropdownLabelBoxStyle(mockRegularLabel)).toEqual(
|
||||
expect.objectContaining({
|
||||
backgroundColor: mockRegularLabel.color,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLabelSelected', () => {
|
||||
it('returns true when provided `label` param is one of the selected labels', () => {
|
||||
expect(wrapper.vm.isLabelSelected(mockRegularLabel)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when provided `label` param is not one of the selected labels', () => {
|
||||
expect(wrapper.vm.isLabelSelected(mockLabels[2])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleKeyDown', () => {
|
||||
it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', () => {
|
||||
wrapper.setData({
|
||||
currentHighlightItem: 1,
|
||||
});
|
||||
|
||||
wrapper.vm.handleKeyDown({
|
||||
keyCode: UP_KEY_CODE,
|
||||
});
|
||||
|
||||
expect(wrapper.vm.currentHighlightItem).toBe(0);
|
||||
});
|
||||
|
||||
it('increases `currentHighlightItem` value by 1 when Down arrow key is pressed', () => {
|
||||
wrapper.setData({
|
||||
currentHighlightItem: 1,
|
||||
});
|
||||
|
||||
wrapper.vm.handleKeyDown({
|
||||
keyCode: DOWN_KEY_CODE,
|
||||
});
|
||||
|
||||
expect(wrapper.vm.currentHighlightItem).toBe(2);
|
||||
});
|
||||
|
||||
it('calls action `updateSelectedLabels` with currently highlighted label when Enter key is pressed', () => {
|
||||
jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation();
|
||||
wrapper.setData({
|
||||
currentHighlightItem: 1,
|
||||
});
|
||||
|
||||
wrapper.vm.handleKeyDown({
|
||||
keyCode: ENTER_KEY_CODE,
|
||||
});
|
||||
|
||||
expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([
|
||||
{
|
||||
...mockLabels[1],
|
||||
set: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('calls action `toggleDropdownContents` when Esc key is pressed', () => {
|
||||
jest.spyOn(wrapper.vm, 'toggleDropdownContents').mockImplementation();
|
||||
wrapper.setData({
|
||||
currentHighlightItem: 1,
|
||||
});
|
||||
|
||||
wrapper.vm.handleKeyDown({
|
||||
keyCode: ESC_KEY_CODE,
|
||||
});
|
||||
|
||||
expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed', () => {
|
||||
jest.spyOn(wrapper.vm, 'scrollIntoViewIfNeeded').mockImplementation();
|
||||
wrapper.setData({
|
||||
currentHighlightItem: 1,
|
||||
});
|
||||
|
||||
wrapper.vm.handleKeyDown({
|
||||
keyCode: DOWN_KEY_CODE,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.vm.scrollIntoViewIfNeeded).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleLabelClick', () => {
|
||||
it('calls action `updateSelectedLabels` with provided `label` param', () => {
|
||||
jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation();
|
||||
|
||||
wrapper.vm.handleLabelClick(mockRegularLabel);
|
||||
|
||||
expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([mockRegularLabel]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('renders component container element with class `labels-select-contents-list`', () => {
|
||||
expect(wrapper.attributes('class')).toContain('labels-select-contents-list');
|
||||
});
|
||||
|
||||
it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', () => {
|
||||
wrapper.vm.$store.dispatch('requestLabels');
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
const loadingIconEl = wrapper.find(GlLoadingIcon);
|
||||
|
||||
expect(loadingIconEl.exists()).toBe(true);
|
||||
expect(loadingIconEl.attributes('class')).toContain('labels-fetch-loading');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders dropdown title element', () => {
|
||||
const titleEl = wrapper.find('.dropdown-title > span');
|
||||
|
||||
expect(titleEl.exists()).toBe(true);
|
||||
expect(titleEl.text()).toBe('Assign labels');
|
||||
});
|
||||
|
||||
it('renders dropdown close button element', () => {
|
||||
const closeButtonEl = wrapper.find('.dropdown-title').find(GlButton);
|
||||
|
||||
expect(closeButtonEl.exists()).toBe(true);
|
||||
expect(closeButtonEl.find(GlIcon).exists()).toBe(true);
|
||||
expect(closeButtonEl.find(GlIcon).props('name')).toBe('close');
|
||||
});
|
||||
|
||||
it('renders label search input element', () => {
|
||||
const searchInputEl = wrapper.find(GlSearchBoxByType);
|
||||
|
||||
expect(searchInputEl.exists()).toBe(true);
|
||||
expect(searchInputEl.attributes('autofocus')).toBe('true');
|
||||
});
|
||||
|
||||
it('renders label elements for all labels', () => {
|
||||
const labelsEl = wrapper.findAll('.dropdown-content li');
|
||||
const labelItemEl = labelsEl.at(0).find(GlLink);
|
||||
|
||||
expect(labelsEl.length).toBe(mockLabels.length);
|
||||
expect(labelItemEl.exists()).toBe(true);
|
||||
expect(labelItemEl.find(GlIcon).props('name')).toBe('mobile-issue-close');
|
||||
expect(labelItemEl.find('.dropdown-label-box').attributes('style')).toBe(
|
||||
'background-color: rgb(186, 218, 85);',
|
||||
);
|
||||
expect(labelItemEl.find(GlLink).text()).toContain(mockLabels[0].title);
|
||||
});
|
||||
|
||||
it('renders label element with "is-focused" when value of `currentHighlightItem` is more than -1', () => {
|
||||
wrapper.setData({
|
||||
currentHighlightItem: 0,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
const labelsEl = wrapper.findAll('.dropdown-content li');
|
||||
const labelItemEl = labelsEl.at(0).find(GlLink);
|
||||
|
||||
expect(labelItemEl.attributes('class')).toContain('is-focused');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders element containing "No matching results" when `searchKey` does not match with any label', () => {
|
||||
wrapper.setData({
|
||||
searchKey: 'abc',
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
const noMatchEl = wrapper.find('.dropdown-content li');
|
||||
|
||||
expect(noMatchEl.exists()).toBe(true);
|
||||
expect(noMatchEl.text()).toContain('No matching results');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders footer list items', () => {
|
||||
const createLabelBtn = wrapper.find('.dropdown-footer').find(GlButton);
|
||||
const manageLabelsLink = wrapper.find('.dropdown-footer').find(GlLink);
|
||||
|
||||
expect(createLabelBtn.exists()).toBe(true);
|
||||
expect(createLabelBtn.text()).toBe('Create label');
|
||||
expect(manageLabelsLink.exists()).toBe(true);
|
||||
expect(manageLabelsLink.text()).toBe('Manage labels');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
|
||||
|
||||
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
|
||||
|
||||
import { mockConfig } from './mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const createComponent = (initialState = mockConfig) => {
|
||||
const store = new Vuex.Store(labelsSelectModule());
|
||||
|
||||
store.dispatch('setInitialState', initialState);
|
||||
|
||||
return shallowMount(DropdownContents, {
|
||||
localVue,
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
||||
describe('DropdownContent', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('computed', () => {
|
||||
describe('dropdownContentsView', () => {
|
||||
it('returns string "dropdown-contents-create-view" when `showDropdownContentsCreateView` prop is `true`', () => {
|
||||
wrapper.vm.$store.dispatch('toggleDropdownContentsCreateView');
|
||||
|
||||
expect(wrapper.vm.dropdownContentsView).toBe('dropdown-contents-create-view');
|
||||
});
|
||||
|
||||
it('returns string "dropdown-contents-labels-view" when `showDropdownContentsCreateView` prop is `false`', () => {
|
||||
expect(wrapper.vm.dropdownContentsView).toBe('dropdown-contents-labels-view');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('renders component container element with class `labels-select-dropdown-contents`', () => {
|
||||
expect(wrapper.attributes('class')).toContain('labels-select-dropdown-contents');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
|
||||
import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
|
||||
|
||||
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
|
||||
|
||||
import { mockConfig } from './mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const createComponent = (initialState = mockConfig) => {
|
||||
const store = new Vuex.Store(labelsSelectModule());
|
||||
|
||||
store.dispatch('setInitialState', initialState);
|
||||
|
||||
return shallowMount(DropdownTitle, {
|
||||
localVue,
|
||||
store,
|
||||
propsData: {
|
||||
labelsSelectInProgress: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('DropdownTitle', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('renders component container element with string "Labels"', () => {
|
||||
expect(wrapper.text()).toContain('Labels');
|
||||
});
|
||||
|
||||
it('renders edit link', () => {
|
||||
const editBtnEl = wrapper.find(GlButton);
|
||||
|
||||
expect(editBtnEl.exists()).toBe(true);
|
||||
expect(editBtnEl.text()).toBe('Edit');
|
||||
});
|
||||
|
||||
it('renders loading icon element when `labelsSelectInProgress` prop is true', () => {
|
||||
wrapper.setProps({
|
||||
labelsSelectInProgress: true,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import { GlLabel } from '@gitlab/ui';
|
||||
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue';
|
||||
|
||||
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
|
||||
|
||||
import { mockConfig, mockRegularLabel, mockScopedLabel } from './mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const createComponent = (initialState = mockConfig, slots = {}) => {
|
||||
const store = new Vuex.Store(labelsSelectModule());
|
||||
|
||||
store.dispatch('setInitialState', initialState);
|
||||
|
||||
return shallowMount(DropdownValue, {
|
||||
localVue,
|
||||
store,
|
||||
slots,
|
||||
});
|
||||
};
|
||||
|
||||
describe('DropdownValue', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('labelFilterUrl', () => {
|
||||
it('returns a label filter URL based on provided label param', () => {
|
||||
expect(wrapper.vm.labelFilterUrl(mockRegularLabel)).toBe(
|
||||
'/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scopedLabel', () => {
|
||||
it('returns `true` when provided label param is a scoped label', () => {
|
||||
expect(wrapper.vm.scopedLabel(mockScopedLabel)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns `false` when provided label param is a regular label', () => {
|
||||
expect(wrapper.vm.scopedLabel(mockRegularLabel)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('renders class `has-labels` on component container element when `selectedLabels` is not empty', () => {
|
||||
expect(wrapper.attributes('class')).toContain('has-labels');
|
||||
});
|
||||
|
||||
it('renders element containing `None` when `selectedLabels` is empty', () => {
|
||||
const wrapperNoLabels = createComponent(
|
||||
{
|
||||
...mockConfig,
|
||||
selectedLabels: [],
|
||||
},
|
||||
{
|
||||
default: 'None',
|
||||
},
|
||||
);
|
||||
const noneEl = wrapperNoLabels.find('span.text-secondary');
|
||||
|
||||
expect(noneEl.exists()).toBe(true);
|
||||
expect(noneEl.text()).toBe('None');
|
||||
|
||||
wrapperNoLabels.destroy();
|
||||
});
|
||||
|
||||
it('renders labels when `selectedLabels` is not empty', () => {
|
||||
expect(wrapper.findAll(GlLabel).length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
|
||||
import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
|
||||
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue';
|
||||
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
|
||||
import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
|
||||
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
|
||||
|
||||
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
|
||||
|
||||
import { mockConfig } from './mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const createComponent = (config = mockConfig, slots = {}) =>
|
||||
shallowMount(LabelsSelectRoot, {
|
||||
localVue,
|
||||
slots,
|
||||
store: new Vuex.Store(labelsSelectModule()),
|
||||
propsData: config,
|
||||
});
|
||||
|
||||
describe('LabelsSelectRoot', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('handleVuexActionDispatch', () => {
|
||||
it('calls `handleDropdownClose` when params `action.type` is `toggleDropdownContents` and state has `showDropdownButton` & `showDropdownContents` props `false`', () => {
|
||||
jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation();
|
||||
|
||||
wrapper.vm.handleVuexActionDispatch(
|
||||
{ type: 'toggleDropdownContents' },
|
||||
{
|
||||
showDropdownButton: false,
|
||||
showDropdownContents: false,
|
||||
labels: [{ id: 1 }, { id: 2, touched: true }],
|
||||
},
|
||||
);
|
||||
|
||||
expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
id: 2,
|
||||
touched: true,
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDropdownClose', () => {
|
||||
it('emits `updateSelectedLabels` & `onDropdownClose` events on component when provided `labels` param is not empty', () => {
|
||||
wrapper.vm.handleDropdownClose([{ id: 1 }, { id: 2 }]);
|
||||
|
||||
expect(wrapper.emitted().updateSelectedLabels).toBeTruthy();
|
||||
expect(wrapper.emitted().onDropdownClose).toBeTruthy();
|
||||
});
|
||||
|
||||
it('emits only `onDropdownClose` event on component when provided `labels` param is empty', () => {
|
||||
wrapper.vm.handleDropdownClose([]);
|
||||
|
||||
expect(wrapper.emitted().updateSelectedLabels).toBeFalsy();
|
||||
expect(wrapper.emitted().onDropdownClose).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCollapsedValueClick', () => {
|
||||
it('emits `toggleCollapse` event on component', () => {
|
||||
wrapper.vm.handleCollapsedValueClick();
|
||||
|
||||
expect(wrapper.emitted().toggleCollapse).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('renders component with classes `labels-select-wrapper position-relative`', () => {
|
||||
expect(wrapper.attributes('class')).toContain('labels-select-wrapper position-relative');
|
||||
});
|
||||
|
||||
it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', () => {
|
||||
expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders `dropdown-title` component', () => {
|
||||
expect(wrapper.find(DropdownTitle).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders `dropdown-value` component with slot when `showDropdownButton` prop is `false`', () => {
|
||||
const wrapperDropdownValue = createComponent(mockConfig, {
|
||||
default: 'None',
|
||||
});
|
||||
|
||||
const valueComp = wrapperDropdownValue.find(DropdownValue);
|
||||
|
||||
expect(valueComp.exists()).toBe(true);
|
||||
expect(valueComp.text()).toBe('None');
|
||||
|
||||
wrapperDropdownValue.destroy();
|
||||
});
|
||||
|
||||
it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', () => {
|
||||
wrapper.vm.$store.dispatch('toggleDropdownButton');
|
||||
|
||||
expect(wrapper.find(DropdownButton).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', () => {
|
||||
wrapper.vm.$store.dispatch('toggleDropdownContents');
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.find(DropdownContents).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
export const mockRegularLabel = {
|
||||
id: 26,
|
||||
title: 'Foo Label',
|
||||
description: 'Foobar',
|
||||
color: '#BADA55',
|
||||
textColor: '#FFFFFF',
|
||||
};
|
||||
|
||||
export const mockScopedLabel = {
|
||||
id: 27,
|
||||
title: 'Foo::Bar',
|
||||
description: 'Foobar',
|
||||
color: '#0033CC',
|
||||
textColor: '#FFFFFF',
|
||||
};
|
||||
|
||||
export const mockLabels = [
|
||||
mockRegularLabel,
|
||||
mockScopedLabel,
|
||||
{
|
||||
id: 28,
|
||||
title: 'Bug',
|
||||
description: 'Label for bugs',
|
||||
color: '#FF0000',
|
||||
textColor: '#FFFFFF',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockConfig = {
|
||||
allowLabelEdit: true,
|
||||
allowLabelCreate: true,
|
||||
allowScopedLabels: true,
|
||||
labelsListTitle: 'Assign labels',
|
||||
labelsCreateTitle: 'Create label',
|
||||
dropdownOnly: false,
|
||||
selectedLabels: [mockRegularLabel, mockScopedLabel],
|
||||
labelsSelectInProgress: false,
|
||||
labelsFetchPath: '/gitlab-org/my-project/-/labels.json',
|
||||
labelsManagePath: '/gitlab-org/my-project/-/labels',
|
||||
labelsFilterBasePath: '/gitlab-org/my-project/issues',
|
||||
scopedLabelsDocumentationPath: '/help/user/project/labels.md#scoped-labels-premium',
|
||||
};
|
||||
|
||||
export const mockSuggestedColors = {
|
||||
'#0033CC': 'UA blue',
|
||||
'#428BCA': 'Moderate blue',
|
||||
'#44AD8E': 'Lime green',
|
||||
'#A8D695': 'Feijoa',
|
||||
'#5CB85C': 'Slightly desaturated green',
|
||||
'#69D100': 'Bright green',
|
||||
'#004E00': 'Very dark lime green',
|
||||
'#34495E': 'Very dark desaturated blue',
|
||||
'#7F8C8D': 'Dark grayish cyan',
|
||||
'#A295D6': 'Slightly desaturated blue',
|
||||
'#5843AD': 'Dark moderate blue',
|
||||
'#8E44AD': 'Dark moderate violet',
|
||||
'#FFECDB': 'Very pale orange',
|
||||
'#AD4363': 'Dark moderate pink',
|
||||
'#D10069': 'Strong pink',
|
||||
'#CC0033': 'Strong red',
|
||||
'#FF0000': 'Pure red',
|
||||
'#D9534F': 'Soft red',
|
||||
'#D1D100': 'Strong yellow',
|
||||
'#F0AD4E': 'Soft orange',
|
||||
'#AD8D43': 'Dark moderate orange',
|
||||
};
|
||||
|
|
@ -0,0 +1,276 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
|
||||
import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
|
||||
import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
|
||||
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
describe('LabelsSelect Actions', () => {
|
||||
let state;
|
||||
const mockInitialState = {
|
||||
labels: [],
|
||||
selectedLabels: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
state = Object.assign({}, defaultState());
|
||||
});
|
||||
|
||||
describe('setInitialState', () => {
|
||||
it('sets initial store state', done => {
|
||||
testAction(
|
||||
actions.setInitialState,
|
||||
mockInitialState,
|
||||
state,
|
||||
[{ type: types.SET_INITIAL_STATE, payload: mockInitialState }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleDropdownButton', () => {
|
||||
it('toggles dropdown button', done => {
|
||||
testAction(
|
||||
actions.toggleDropdownButton,
|
||||
{},
|
||||
state,
|
||||
[{ type: types.TOGGLE_DROPDOWN_BUTTON }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleDropdownContents', () => {
|
||||
it('toggles dropdown contents', done => {
|
||||
testAction(
|
||||
actions.toggleDropdownContents,
|
||||
{},
|
||||
state,
|
||||
[{ type: types.TOGGLE_DROPDOWN_CONTENTS }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleDropdownContentsCreateView', () => {
|
||||
it('toggles dropdown create view', done => {
|
||||
testAction(
|
||||
actions.toggleDropdownContentsCreateView,
|
||||
{},
|
||||
state,
|
||||
[{ type: types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestLabels', () => {
|
||||
it('sets value of `state.labelsFetchInProgress` to `true`', done => {
|
||||
testAction(actions.requestLabels, {}, state, [{ type: types.REQUEST_LABELS }], [], done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveLabelsSuccess', () => {
|
||||
it('sets provided labels to `state.labels`', done => {
|
||||
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
|
||||
|
||||
testAction(
|
||||
actions.receiveLabelsSuccess,
|
||||
labels,
|
||||
state,
|
||||
[{ type: types.RECEIVE_SET_LABELS_SUCCESS, payload: labels }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveLabelsFailure', () => {
|
||||
beforeEach(() => {
|
||||
setFixtures('<div class="flash-container"></div>');
|
||||
});
|
||||
|
||||
it('sets value `state.labelsFetchInProgress` to `false`', done => {
|
||||
testAction(
|
||||
actions.receiveLabelsFailure,
|
||||
{},
|
||||
state,
|
||||
[{ type: types.RECEIVE_SET_LABELS_FAILURE }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('shows flash error', () => {
|
||||
actions.receiveLabelsFailure({ commit: () => {} });
|
||||
|
||||
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
|
||||
'Error fetching labels.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchLabels', () => {
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
state.labelsFetchPath = 'labels.json';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('on success', () => {
|
||||
it('dispatches `requestLabels` & `receiveLabelsSuccess` actions', done => {
|
||||
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
|
||||
mock.onGet(/labels.json/).replyOnce(200, labels);
|
||||
|
||||
testAction(
|
||||
actions.fetchLabels,
|
||||
{},
|
||||
state,
|
||||
[],
|
||||
[{ type: 'requestLabels' }, { type: 'receiveLabelsSuccess', payload: labels }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on failure', () => {
|
||||
it('dispatches `requestLabels` & `receiveLabelsFailure` actions', done => {
|
||||
mock.onGet(/labels.json/).replyOnce(500, {});
|
||||
|
||||
testAction(
|
||||
actions.fetchLabels,
|
||||
{},
|
||||
state,
|
||||
[],
|
||||
[{ type: 'requestLabels' }, { type: 'receiveLabelsFailure' }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestCreateLabel', () => {
|
||||
it('sets value `state.labelCreateInProgress` to `true`', done => {
|
||||
testAction(
|
||||
actions.requestCreateLabel,
|
||||
{},
|
||||
state,
|
||||
[{ type: types.REQUEST_CREATE_LABEL }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveCreateLabelSuccess', () => {
|
||||
it('sets value `state.labelCreateInProgress` to `false`', done => {
|
||||
testAction(
|
||||
actions.receiveCreateLabelSuccess,
|
||||
{},
|
||||
state,
|
||||
[{ type: types.RECEIVE_CREATE_LABEL_SUCCESS }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveCreateLabelFailure', () => {
|
||||
beforeEach(() => {
|
||||
setFixtures('<div class="flash-container"></div>');
|
||||
});
|
||||
|
||||
it('sets value `state.labelCreateInProgress` to `false`', done => {
|
||||
testAction(
|
||||
actions.receiveCreateLabelFailure,
|
||||
{},
|
||||
state,
|
||||
[{ type: types.RECEIVE_CREATE_LABEL_FAILURE }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('shows flash error', () => {
|
||||
actions.receiveCreateLabelFailure({ commit: () => {} });
|
||||
|
||||
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
|
||||
'Error creating label.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createLabel', () => {
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
state.labelsManagePath = 'labels.json';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('on success', () => {
|
||||
it('dispatches `requestCreateLabel`, `receiveCreateLabelSuccess` & `toggleDropdownContentsCreateView` actions', done => {
|
||||
const label = { id: 1 };
|
||||
mock.onPost(/labels.json/).replyOnce(200, label);
|
||||
|
||||
testAction(
|
||||
actions.createLabel,
|
||||
{},
|
||||
state,
|
||||
[],
|
||||
[
|
||||
{ type: 'requestCreateLabel' },
|
||||
{ type: 'receiveCreateLabelSuccess' },
|
||||
{ type: 'toggleDropdownContentsCreateView' },
|
||||
],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on failure', () => {
|
||||
it('dispatches `requestCreateLabel` & `receiveCreateLabelFailure` actions', done => {
|
||||
mock.onPost(/labels.json/).replyOnce(500, {});
|
||||
|
||||
testAction(
|
||||
actions.createLabel,
|
||||
{},
|
||||
state,
|
||||
[],
|
||||
[{ type: 'requestCreateLabel' }, { type: 'receiveCreateLabelFailure' }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSelectedLabels', () => {
|
||||
it('updates `state.labels` based on provided `labels` param', done => {
|
||||
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
|
||||
|
||||
testAction(
|
||||
actions.updateSelectedLabels,
|
||||
labels,
|
||||
state,
|
||||
[{ type: types.UPDATE_SELECTED_LABELS, payload: { labels } }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import * as getters from '~/vue_shared/components/sidebar/labels_select_vue/store/getters';
|
||||
|
||||
describe('LabelsSelect Getters', () => {
|
||||
describe('dropdownButtonText', () => {
|
||||
it('returns string "Label" when state.labels has no selected labels', () => {
|
||||
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
|
||||
|
||||
expect(getters.dropdownButtonText({ labels })).toBe('Label');
|
||||
});
|
||||
|
||||
it('returns label title when state.labels has only 1 label', () => {
|
||||
const labels = [{ id: 1, title: 'Foobar', set: true }];
|
||||
|
||||
expect(getters.dropdownButtonText({ labels })).toBe('Foobar');
|
||||
});
|
||||
|
||||
it('returns first label title and remaining labels count when state.labels has more than 1 label', () => {
|
||||
const labels = [{ id: 1, title: 'Foo', set: true }, { id: 2, title: 'Bar', set: true }];
|
||||
|
||||
expect(getters.dropdownButtonText({ labels })).toBe('Foo +1 more');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectedLabelsList', () => {
|
||||
it('returns array of IDs of all labels within `state.selectedLabels`', () => {
|
||||
const selectedLabels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
|
||||
|
||||
expect(getters.selectedLabelsList({ selectedLabels })).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
|
||||
import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
|
||||
|
||||
describe('LabelsSelect Mutations', () => {
|
||||
describe(`${types.SET_INITIAL_STATE}`, () => {
|
||||
it('initializes provided props to store state', () => {
|
||||
const state = {};
|
||||
mutations[types.SET_INITIAL_STATE](state, {
|
||||
labels: 'foo',
|
||||
});
|
||||
|
||||
expect(state.labels).toEqual('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${types.TOGGLE_DROPDOWN_BUTTON}`, () => {
|
||||
it('toggles value of `state.showDropdownButton`', () => {
|
||||
const state = {
|
||||
showDropdownButton: false,
|
||||
};
|
||||
mutations[types.TOGGLE_DROPDOWN_BUTTON](state);
|
||||
|
||||
expect(state.showDropdownButton).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${types.TOGGLE_DROPDOWN_CONTENTS}`, () => {
|
||||
it('toggles value of `state.showDropdownButton` when `state.dropdownOnly` is false', () => {
|
||||
const state = {
|
||||
dropdownOnly: false,
|
||||
showDropdownButton: false,
|
||||
};
|
||||
mutations[types.TOGGLE_DROPDOWN_CONTENTS](state);
|
||||
|
||||
expect(state.showDropdownButton).toBe(true);
|
||||
});
|
||||
|
||||
it('toggles value of `state.showDropdownContents`', () => {
|
||||
const state = {
|
||||
showDropdownContents: false,
|
||||
};
|
||||
mutations[types.TOGGLE_DROPDOWN_CONTENTS](state);
|
||||
|
||||
expect(state.showDropdownContents).toBe(true);
|
||||
});
|
||||
|
||||
it('sets value of `state.showDropdownContentsCreateView` to `false` when `showDropdownContents` is true', () => {
|
||||
const state = {
|
||||
showDropdownContents: false,
|
||||
showDropdownContentsCreateView: true,
|
||||
};
|
||||
mutations[types.TOGGLE_DROPDOWN_CONTENTS](state);
|
||||
|
||||
expect(state.showDropdownContentsCreateView).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW}`, () => {
|
||||
it('toggles value of `state.showDropdownContentsCreateView`', () => {
|
||||
const state = {
|
||||
showDropdownContentsCreateView: false,
|
||||
};
|
||||
mutations[types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW](state);
|
||||
|
||||
expect(state.showDropdownContentsCreateView).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${types.REQUEST_LABELS}`, () => {
|
||||
it('sets value of `state.labelsFetchInProgress` to true', () => {
|
||||
const state = {
|
||||
labelsFetchInProgress: false,
|
||||
};
|
||||
mutations[types.REQUEST_LABELS](state);
|
||||
|
||||
expect(state.labelsFetchInProgress).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${types.RECEIVE_SET_LABELS_SUCCESS}`, () => {
|
||||
const selectedLabels = [{ id: 2 }, { id: 4 }];
|
||||
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
|
||||
|
||||
it('sets value of `state.labelsFetchInProgress` to false', () => {
|
||||
const state = {
|
||||
selectedLabels,
|
||||
labelsFetchInProgress: true,
|
||||
};
|
||||
mutations[types.RECEIVE_SET_LABELS_SUCCESS](state, labels);
|
||||
|
||||
expect(state.labelsFetchInProgress).toBe(false);
|
||||
});
|
||||
|
||||
it('sets provided `labels` to `state.labels` along with `set` prop based on `state.selectedLabels`', () => {
|
||||
const selectedLabelIds = selectedLabels.map(label => label.id);
|
||||
const state = {
|
||||
selectedLabels,
|
||||
labelsFetchInProgress: true,
|
||||
};
|
||||
mutations[types.RECEIVE_SET_LABELS_SUCCESS](state, labels);
|
||||
|
||||
state.labels.forEach(label => {
|
||||
if (selectedLabelIds.includes(label.id)) {
|
||||
expect(label.set).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${types.RECEIVE_SET_LABELS_FAILURE}`, () => {
|
||||
it('sets value of `state.labelsFetchInProgress` to false', () => {
|
||||
const state = {
|
||||
labelsFetchInProgress: true,
|
||||
};
|
||||
mutations[types.RECEIVE_SET_LABELS_FAILURE](state);
|
||||
|
||||
expect(state.labelsFetchInProgress).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${types.REQUEST_CREATE_LABEL}`, () => {
|
||||
it('sets value of `state.labelCreateInProgress` to true', () => {
|
||||
const state = {
|
||||
labelCreateInProgress: false,
|
||||
};
|
||||
mutations[types.REQUEST_CREATE_LABEL](state);
|
||||
|
||||
expect(state.labelCreateInProgress).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${types.RECEIVE_CREATE_LABEL_SUCCESS}`, () => {
|
||||
it('sets value of `state.labelCreateInProgress` to false', () => {
|
||||
const state = {
|
||||
labelCreateInProgress: false,
|
||||
};
|
||||
mutations[types.RECEIVE_CREATE_LABEL_SUCCESS](state);
|
||||
|
||||
expect(state.labelCreateInProgress).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${types.RECEIVE_CREATE_LABEL_FAILURE}`, () => {
|
||||
it('sets value of `state.labelCreateInProgress` to false', () => {
|
||||
const state = {
|
||||
labelCreateInProgress: false,
|
||||
};
|
||||
mutations[types.RECEIVE_CREATE_LABEL_FAILURE](state);
|
||||
|
||||
expect(state.labelCreateInProgress).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
|
||||
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
|
||||
|
||||
it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => {
|
||||
const updatedLabelIds = [2, 4];
|
||||
const state = {
|
||||
labels,
|
||||
};
|
||||
mutations[types.UPDATE_SELECTED_LABELS](state, { labels });
|
||||
|
||||
state.labels.forEach(label => {
|
||||
if (updatedLabelIds.includes(label.id)) {
|
||||
expect(label.touched).toBe(true);
|
||||
expect(label.set).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -601,10 +601,23 @@ describe Snippet do
|
|||
expect(snippet.create_repository).to be_nil
|
||||
end
|
||||
|
||||
it 'does not track snippet repository' do
|
||||
expect do
|
||||
snippet.create_repository
|
||||
end.not_to change(SnippetRepository, :count)
|
||||
context 'when snippet_repository exists' do
|
||||
it 'does not create a new snippet repository' do
|
||||
expect do
|
||||
snippet.create_repository
|
||||
end.not_to change(SnippetRepository, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when snippet_repository does not exist' do
|
||||
it 'creates a snippet_repository' do
|
||||
snippet.snippet_repository.destroy
|
||||
snippet.reload
|
||||
|
||||
expect do
|
||||
snippet.create_repository
|
||||
end.to change(SnippetRepository, :count).by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ describe 'Updating a Snippet' do
|
|||
|
||||
describe 'PersonalSnippet' do
|
||||
it_behaves_like 'graphql update actions' do
|
||||
let_it_be(:snippet) do
|
||||
let(:snippet) do
|
||||
create(:personal_snippet,
|
||||
:private,
|
||||
file_name: original_file_name,
|
||||
|
|
@ -104,7 +104,7 @@ describe 'Updating a Snippet' do
|
|||
|
||||
describe 'ProjectSnippet' do
|
||||
let_it_be(:project) { create(:project, :private) }
|
||||
let_it_be(:snippet) do
|
||||
let(:snippet) do
|
||||
create(:project_snippet,
|
||||
:private,
|
||||
project: project,
|
||||
|
|
|
|||
|
|
@ -278,13 +278,13 @@ describe API::ProjectSnippets do
|
|||
|
||||
describe 'PUT /projects/:project_id/snippets/:id/' do
|
||||
let(:visibility_level) { Snippet::PUBLIC }
|
||||
let(:snippet) { create(:project_snippet, author: admin, visibility_level: visibility_level, project: project) }
|
||||
let(:snippet) { create(:project_snippet, :repository, author: admin, visibility_level: visibility_level, project: project) }
|
||||
|
||||
it 'updates snippet' do
|
||||
new_content = 'New content'
|
||||
new_description = 'New description'
|
||||
|
||||
put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), params: { code: new_content, description: new_description, visibility: 'private' }
|
||||
update_snippet(params: { code: new_content, description: new_description, visibility: 'private' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
snippet.reload
|
||||
|
|
@ -297,7 +297,7 @@ describe API::ProjectSnippets do
|
|||
new_content = 'New content'
|
||||
new_description = 'New description'
|
||||
|
||||
put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), params: { content: new_content, description: new_description }
|
||||
update_snippet(params: { content: new_content, description: new_description })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
snippet.reload
|
||||
|
|
@ -306,21 +306,21 @@ describe API::ProjectSnippets do
|
|||
end
|
||||
|
||||
it 'returns 400 when both code and content parameters specified' do
|
||||
put api("/projects/#{snippet.project.id}/snippets/1234", admin), params: { code: 'some content', content: 'other content' }
|
||||
update_snippet(params: { code: 'some content', content: 'other content' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['error']).to eq('code, content are mutually exclusive')
|
||||
end
|
||||
|
||||
it 'returns 404 for invalid snippet id' do
|
||||
put api("/projects/#{snippet.project.id}/snippets/1234", admin), params: { title: 'foo' }
|
||||
update_snippet(snippet_id: '1234', params: { title: 'foo' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['message']).to eq('404 Snippet Not Found')
|
||||
end
|
||||
|
||||
it 'returns 400 for missing parameters' do
|
||||
put api("/projects/#{project.id}/snippets/1234", admin)
|
||||
update_snippet
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
|
@ -328,16 +328,16 @@ describe API::ProjectSnippets do
|
|||
it 'returns 400 for empty code field' do
|
||||
new_content = ''
|
||||
|
||||
put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), params: { code: new_content }
|
||||
update_snippet(params: { code: new_content })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
context 'when the snippet is spam' do
|
||||
def update_snippet(snippet_params = {})
|
||||
put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin), params: snippet_params
|
||||
end
|
||||
it_behaves_like 'update with repository actions' do
|
||||
let(:snippet_without_repo) { create(:project_snippet, author: admin, project: project, visibility_level: visibility_level) }
|
||||
end
|
||||
|
||||
context 'when the snippet is spam' do
|
||||
before do
|
||||
allow_next_instance_of(Spam::AkismetService) do |instance|
|
||||
allow(instance).to receive(:spam?).and_return(true)
|
||||
|
|
@ -348,7 +348,7 @@ describe API::ProjectSnippets do
|
|||
let(:visibility_level) { Snippet::PRIVATE }
|
||||
|
||||
it 'creates the snippet' do
|
||||
expect { update_snippet(title: 'Foo') }
|
||||
expect { update_snippet(params: { title: 'Foo' }) }
|
||||
.to change { snippet.reload.title }.to('Foo')
|
||||
end
|
||||
end
|
||||
|
|
@ -357,12 +357,12 @@ describe API::ProjectSnippets do
|
|||
let(:visibility_level) { Snippet::PUBLIC }
|
||||
|
||||
it 'rejects the snippet' do
|
||||
expect { update_snippet(title: 'Foo') }
|
||||
expect { update_snippet(params: { title: 'Foo' }) }
|
||||
.not_to change { snippet.reload.title }
|
||||
end
|
||||
|
||||
it 'creates a spam log' do
|
||||
expect { update_snippet(title: 'Foo') }
|
||||
expect { update_snippet(params: { title: 'Foo' }) }
|
||||
.to log_spam(title: 'Foo', user_id: admin.id, noteable_type: 'ProjectSnippet')
|
||||
end
|
||||
end
|
||||
|
|
@ -371,7 +371,7 @@ describe API::ProjectSnippets do
|
|||
let(:visibility_level) { Snippet::PRIVATE }
|
||||
|
||||
it 'rejects the snippet' do
|
||||
expect { update_snippet(title: 'Foo', visibility: 'public') }
|
||||
expect { update_snippet(params: { title: 'Foo', visibility: 'public' }) }
|
||||
.not_to change { snippet.reload.title }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
|
|
@ -379,7 +379,7 @@ describe API::ProjectSnippets do
|
|||
end
|
||||
|
||||
it 'creates a spam log' do
|
||||
expect { update_snippet(title: 'Foo', visibility: 'public') }
|
||||
expect { update_snippet(params: { title: 'Foo', visibility: 'public' }) }
|
||||
.to log_spam(title: 'Foo', user_id: admin.id, noteable_type: 'ProjectSnippet')
|
||||
end
|
||||
end
|
||||
|
|
@ -390,6 +390,10 @@ describe API::ProjectSnippets do
|
|||
let(:request) { put api("/projects/#{project_no_snippets.id}/snippets/123", admin), params: { description: 'foo' } }
|
||||
end
|
||||
end
|
||||
|
||||
def update_snippet(snippet_id: snippet.id, params: {})
|
||||
put api("/projects/#{snippet.project.id}/snippets/#{snippet_id}", admin), params: params
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /projects/:project_id/snippets/:id/' do
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ describe API::Snippets do
|
|||
let(:visibility_level) { Snippet::PUBLIC }
|
||||
let(:other_user) { create(:user) }
|
||||
let(:snippet) do
|
||||
create(:personal_snippet, author: user, visibility_level: visibility_level)
|
||||
create(:personal_snippet, :repository, author: user, visibility_level: visibility_level)
|
||||
end
|
||||
|
||||
shared_examples 'snippet updates' do
|
||||
|
|
@ -309,7 +309,7 @@ describe API::Snippets do
|
|||
new_content = 'New content'
|
||||
new_description = 'New description'
|
||||
|
||||
put api("/snippets/#{snippet.id}", user), params: { content: new_content, description: new_description, visibility: 'internal' }
|
||||
update_snippet(params: { content: new_content, description: new_description, visibility: 'internal' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
snippet.reload
|
||||
|
|
@ -332,30 +332,30 @@ describe API::Snippets do
|
|||
it_behaves_like 'snippet updates'
|
||||
|
||||
it 'returns 404 for invalid snippet id' do
|
||||
put api("/snippets/1234", user), params: { title: 'foo' }
|
||||
update_snippet(snippet_id: '1234', params: { title: 'Foo' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['message']).to eq('404 Snippet Not Found')
|
||||
end
|
||||
|
||||
it "returns 404 for another user's snippet" do
|
||||
put api("/snippets/#{snippet.id}", other_user), params: { title: 'fubar' }
|
||||
update_snippet(requester: other_user, params: { title: 'foobar' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['message']).to eq('404 Snippet Not Found')
|
||||
end
|
||||
|
||||
it 'returns 400 for missing parameters' do
|
||||
put api("/snippets/1234", user)
|
||||
update_snippet
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
context 'when the snippet is spam' do
|
||||
def update_snippet(snippet_params = {})
|
||||
put api("/snippets/#{snippet.id}", user), params: snippet_params
|
||||
end
|
||||
it_behaves_like 'update with repository actions' do
|
||||
let(:snippet_without_repo) { create(:personal_snippet, author: user, visibility_level: visibility_level) }
|
||||
end
|
||||
|
||||
context 'when the snippet is spam' do
|
||||
before do
|
||||
allow_next_instance_of(Spam::AkismetService) do |instance|
|
||||
allow(instance).to receive(:spam?).and_return(true)
|
||||
|
|
@ -366,7 +366,7 @@ describe API::Snippets do
|
|||
let(:visibility_level) { Snippet::PRIVATE }
|
||||
|
||||
it 'updates the snippet' do
|
||||
expect { update_snippet(title: 'Foo') }
|
||||
expect { update_snippet(params: { title: 'Foo' }) }
|
||||
.to change { snippet.reload.title }.to('Foo')
|
||||
end
|
||||
end
|
||||
|
|
@ -375,7 +375,7 @@ describe API::Snippets do
|
|||
let(:visibility_level) { Snippet::PUBLIC }
|
||||
|
||||
it 'rejects the shippet' do
|
||||
expect { update_snippet(title: 'Foo') }
|
||||
expect { update_snippet(params: { title: 'Foo' }) }
|
||||
.not_to change { snippet.reload.title }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
|
|
@ -383,7 +383,7 @@ describe API::Snippets do
|
|||
end
|
||||
|
||||
it 'creates a spam log' do
|
||||
expect { update_snippet(title: 'Foo') }.to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'PersonalSnippet')
|
||||
expect { update_snippet(params: { title: 'Foo' }) }.to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'PersonalSnippet')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -391,16 +391,20 @@ describe API::Snippets do
|
|||
let(:visibility_level) { Snippet::PRIVATE }
|
||||
|
||||
it 'rejects the snippet' do
|
||||
expect { update_snippet(title: 'Foo', visibility: 'public') }
|
||||
expect { update_snippet(params: { title: 'Foo', visibility: 'public' }) }
|
||||
.not_to change { snippet.reload.title }
|
||||
end
|
||||
|
||||
it 'creates a spam log' do
|
||||
expect { update_snippet(title: 'Foo', visibility: 'public') }
|
||||
expect { update_snippet(params: { title: 'Foo', visibility: 'public' }) }
|
||||
.to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'PersonalSnippet')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update_snippet(snippet_id: snippet.id, params: {}, requester: user)
|
||||
put api("/snippets/#{snippet_id}", requester), params: params
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /snippets/:id' do
|
||||
|
|
|
|||
|
|
@ -16,14 +16,9 @@ describe Snippets::UpdateService do
|
|||
}
|
||||
end
|
||||
let(:updater) { user }
|
||||
let(:service) { Snippets::UpdateService.new(project, updater, options) }
|
||||
|
||||
subject do
|
||||
described_class.new(
|
||||
project,
|
||||
updater,
|
||||
options
|
||||
).execute(snippet)
|
||||
end
|
||||
subject { service.execute(snippet) }
|
||||
|
||||
shared_examples 'a service that updates a snippet' do
|
||||
it 'updates a snippet with the provided attributes' do
|
||||
|
|
@ -98,9 +93,109 @@ describe Snippets::UpdateService do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'creates repository and creates file' do
|
||||
it 'creates repository' do
|
||||
expect(snippet.repository).not_to exist
|
||||
|
||||
subject
|
||||
|
||||
expect(snippet.repository).to exist
|
||||
end
|
||||
|
||||
it 'commits the files to the repository' do
|
||||
subject
|
||||
|
||||
expect(snippet.blobs.count).to eq 1
|
||||
|
||||
blob = snippet.repository.blob_at('master', options[:file_name])
|
||||
|
||||
expect(blob.data).to eq options[:content]
|
||||
end
|
||||
|
||||
context 'when the repository does not exist' do
|
||||
it 'does not try to commit file' do
|
||||
allow(snippet).to receive(:repository_exists?).and_return(false)
|
||||
|
||||
expect(service).not_to receive(:create_commit)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(version_snippets: false)
|
||||
end
|
||||
|
||||
it 'does not create repository' do
|
||||
subject
|
||||
|
||||
expect(snippet.repository).not_to exist
|
||||
end
|
||||
|
||||
it 'does not try to commit file' do
|
||||
expect(service).not_to receive(:create_commit)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns error when the commit action fails' do
|
||||
allow_next_instance_of(SnippetRepository) do |instance|
|
||||
allow(instance).to receive(:multi_files_action).and_raise(SnippetRepository::CommitError)
|
||||
end
|
||||
|
||||
response = subject
|
||||
|
||||
expect(response).to be_error
|
||||
expect(response.payload[:snippet].errors.full_messages).to eq ['Error updating the snippet']
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'updates repository content' do
|
||||
it 'commit the files to the repository' do
|
||||
blob = snippet.blobs.first
|
||||
options[:file_name] = blob.path + '_new'
|
||||
|
||||
expect(blob.data).not_to eq(options[:content])
|
||||
|
||||
subject
|
||||
|
||||
blob = snippet.blobs.first
|
||||
|
||||
expect(blob.path).to eq(options[:file_name])
|
||||
expect(blob.data).to eq(options[:content])
|
||||
end
|
||||
|
||||
it 'returns error when the commit action fails' do
|
||||
allow(snippet.snippet_repository).to receive(:multi_files_action).and_raise(SnippetRepository::CommitError)
|
||||
|
||||
response = subject
|
||||
|
||||
expect(response).to be_error
|
||||
expect(response.payload[:snippet].errors.full_messages).to eq ['Error updating the snippet']
|
||||
end
|
||||
|
||||
it 'returns error if snippet does not have a snippet_repository' do
|
||||
allow(snippet).to receive(:snippet_repository).and_return(nil)
|
||||
|
||||
expect(subject).to be_error
|
||||
end
|
||||
|
||||
context 'when the repository does not exist' do
|
||||
it 'does not try to commit file' do
|
||||
allow(snippet).to receive(:repository_exists?).and_return(false)
|
||||
|
||||
expect(service).not_to receive(:create_commit)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Project Snippet' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let!(:snippet) { create(:project_snippet, author: user, project: project) }
|
||||
let!(:snippet) { create(:project_snippet, :repository, author: user, project: project) }
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
|
|
@ -109,15 +204,29 @@ describe Snippets::UpdateService do
|
|||
it_behaves_like 'a service that updates a snippet'
|
||||
it_behaves_like 'public visibility level restrictions apply'
|
||||
it_behaves_like 'snippet update data is tracked'
|
||||
it_behaves_like 'updates repository content'
|
||||
|
||||
context 'when snippet does not have a repository' do
|
||||
let!(:snippet) { create(:project_snippet, author: user, project: project) }
|
||||
|
||||
it_behaves_like 'creates repository and creates file'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when PersonalSnippet' do
|
||||
let(:project) { nil }
|
||||
let!(:snippet) { create(:personal_snippet, author: user) }
|
||||
let!(:snippet) { create(:personal_snippet, :repository, author: user) }
|
||||
|
||||
it_behaves_like 'a service that updates a snippet'
|
||||
it_behaves_like 'public visibility level restrictions apply'
|
||||
it_behaves_like 'snippet update data is tracked'
|
||||
it_behaves_like 'updates repository content'
|
||||
|
||||
context 'when snippet does not have a repository' do
|
||||
let!(:snippet) { create(:personal_snippet, author: user, project: project) }
|
||||
|
||||
it_behaves_like 'creates repository and creates file'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'update with repository actions' do
|
||||
context 'when the repository exists' do
|
||||
it 'commits the changes to the repository' do
|
||||
existing_blob = snippet.blobs.first
|
||||
new_file_name = existing_blob.path + '_new'
|
||||
new_content = 'New content'
|
||||
|
||||
update_snippet(params: { content: new_content, file_name: new_file_name })
|
||||
|
||||
aggregate_failures do
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(snippet.repository.blob_at('master', existing_blob.path)).to be_nil
|
||||
|
||||
blob = snippet.repository.blob_at('master', new_file_name)
|
||||
expect(blob).not_to be_nil
|
||||
expect(blob.data).to eq(new_content)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the repository does not exist' do
|
||||
let(:snippet) { snippet_without_repo }
|
||||
|
||||
it 'creates the repository' do
|
||||
update_snippet(snippet_id: snippet.id, params: { title: 'foo' })
|
||||
|
||||
expect(snippet.repository).to exist
|
||||
end
|
||||
|
||||
it 'commits the file to the repository' do
|
||||
content = 'New Content'
|
||||
file_name = 'file_name.rb'
|
||||
|
||||
update_snippet(snippet_id: snippet.id, params: { content: content, file_name: file_name })
|
||||
|
||||
blob = snippet.repository.blob_at('master', file_name)
|
||||
expect(blob).not_to be_nil
|
||||
expect(blob.data).to eq content
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue