Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
536045b147
commit
3ba6a5a16d
|
@ -1 +1 @@
|
|||
3822772ed121b764fdcc5011d199c37ef76e06a9
|
||||
ac2235fe44c106e9f69b6614ecb72b67421fd402
|
||||
|
|
|
@ -113,31 +113,31 @@ export function formatIssueInput(issueInput, boardConfig) {
|
|||
};
|
||||
}
|
||||
|
||||
export function moveIssueListHelper(issue, fromList, toList) {
|
||||
const updatedIssue = issue;
|
||||
export function moveItemListHelper(item, fromList, toList) {
|
||||
const updatedItem = item;
|
||||
if (
|
||||
toList.listType === ListType.label &&
|
||||
!updatedIssue.labels.find((label) => label.id === toList.label.id)
|
||||
!updatedItem.labels.find((label) => label.id === toList.label.id)
|
||||
) {
|
||||
updatedIssue.labels.push(toList.label);
|
||||
updatedItem.labels.push(toList.label);
|
||||
}
|
||||
if (fromList?.label && fromList.listType === ListType.label) {
|
||||
updatedIssue.labels = updatedIssue.labels.filter((label) => fromList.label.id !== label.id);
|
||||
updatedItem.labels = updatedItem.labels.filter((label) => fromList.label.id !== label.id);
|
||||
}
|
||||
|
||||
if (
|
||||
toList.listType === ListType.assignee &&
|
||||
!updatedIssue.assignees.find((assignee) => assignee.id === toList.assignee.id)
|
||||
!updatedItem.assignees.find((assignee) => assignee.id === toList.assignee.id)
|
||||
) {
|
||||
updatedIssue.assignees.push(toList.assignee);
|
||||
updatedItem.assignees.push(toList.assignee);
|
||||
}
|
||||
if (fromList?.assignee && fromList.listType === ListType.assignee) {
|
||||
updatedIssue.assignees = updatedIssue.assignees.filter(
|
||||
updatedItem.assignees = updatedItem.assignees.filter(
|
||||
(assignee) => assignee.id !== fromList.assignee.id,
|
||||
);
|
||||
}
|
||||
|
||||
return updatedIssue;
|
||||
return updatedItem;
|
||||
}
|
||||
|
||||
export function isListDraggable(list) {
|
||||
|
|
|
@ -13,7 +13,7 @@ export default {
|
|||
default: () => ({}),
|
||||
required: false,
|
||||
},
|
||||
issue: {
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
required: false,
|
||||
|
@ -33,12 +33,12 @@ export default {
|
|||
...mapState(['selectedBoardItems', 'activeId']),
|
||||
...mapGetters(['isSwimlanesOn']),
|
||||
isActive() {
|
||||
return this.issue.id === this.activeId;
|
||||
return this.item.id === this.activeId;
|
||||
},
|
||||
multiSelectVisible() {
|
||||
return (
|
||||
!this.activeId &&
|
||||
this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.issue.id) > -1
|
||||
this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.item.id) > -1
|
||||
);
|
||||
},
|
||||
},
|
||||
|
@ -50,9 +50,9 @@ export default {
|
|||
|
||||
const isMultiSelect = e.ctrlKey || e.metaKey;
|
||||
if (isMultiSelect) {
|
||||
this.toggleBoardItemMultiSelection(this.issue);
|
||||
this.toggleBoardItemMultiSelection(this.item);
|
||||
} else {
|
||||
this.toggleBoardItem({ boardItem: this.issue });
|
||||
this.toggleBoardItem({ boardItem: this.item });
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -64,18 +64,18 @@ export default {
|
|||
data-qa-selector="board_card"
|
||||
:class="{
|
||||
'multi-select': multiSelectVisible,
|
||||
'user-can-drag': !disabled && issue.id,
|
||||
'is-disabled': disabled || !issue.id,
|
||||
'user-can-drag': !disabled && item.id,
|
||||
'is-disabled': disabled || !item.id,
|
||||
'is-active': isActive,
|
||||
}"
|
||||
:index="index"
|
||||
:data-issue-id="issue.id"
|
||||
:data-issue-iid="issue.iid"
|
||||
:data-issue-path="issue.referencePath"
|
||||
:data-item-id="item.id"
|
||||
:data-item-iid="item.iid"
|
||||
:data-item-path="item.referencePath"
|
||||
data-testid="board_card"
|
||||
class="board-card gl-p-5 gl-rounded-base"
|
||||
@mouseup="toggleIssue($event)"
|
||||
>
|
||||
<board-card-inner :list="list" :item="issue" :update-filters="true" />
|
||||
<board-card-inner :list="list" :item="item" :update-filters="true" />
|
||||
</li>
|
||||
</template>
|
||||
|
|
|
@ -49,7 +49,7 @@ export default {
|
|||
: this.lists;
|
||||
},
|
||||
canDragColumns() {
|
||||
return this.glFeatures.graphqlBoardLists && this.canAdminList;
|
||||
return !this.isEpicBoard && this.glFeatures.graphqlBoardLists && this.canAdminList;
|
||||
},
|
||||
boardColumnWrapper() {
|
||||
return this.canDragColumns ? Draggable : 'div';
|
||||
|
@ -80,6 +80,7 @@ export default {
|
|||
|
||||
handleDragOnEnd(params) {
|
||||
sortableEnd();
|
||||
if (this.isEpicBoard) return;
|
||||
|
||||
const { item, newIndex, oldIndex, to } = params;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import Draggable from 'vuedraggable';
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_options';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import defaultSortableConfig from '~/sortable/sortable_config';
|
||||
|
@ -49,7 +49,6 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState(['pageInfoByListId', 'listsFlags']),
|
||||
...mapGetters(['isEpicBoard']),
|
||||
paginatedIssueText() {
|
||||
return sprintf(__('Showing %{pageSize} of %{total} issues'), {
|
||||
pageSize: this.boardItems.length,
|
||||
|
@ -70,13 +69,13 @@ export default {
|
|||
},
|
||||
listRef() {
|
||||
// When list is draggable, the reference to the list needs to be accessed differently
|
||||
return this.canAdminList && !this.isEpicBoard ? this.$refs.list.$el : this.$refs.list;
|
||||
return this.canAdminList ? this.$refs.list.$el : this.$refs.list;
|
||||
},
|
||||
showingAllIssues() {
|
||||
return this.boardItems.length === this.list.issuesCount;
|
||||
},
|
||||
treeRootWrapper() {
|
||||
return this.canAdminList && !this.isEpicBoard ? Draggable : 'ul';
|
||||
return this.canAdminList ? Draggable : 'ul';
|
||||
},
|
||||
treeRootOptions() {
|
||||
const options = {
|
||||
|
@ -113,7 +112,7 @@ export default {
|
|||
this.listRef.removeEventListener('scroll', this.onScroll);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchItemsForList', 'moveIssue']),
|
||||
...mapActions(['fetchItemsForList', 'moveItem']),
|
||||
listHeight() {
|
||||
return this.listRef.getBoundingClientRect().height;
|
||||
},
|
||||
|
@ -149,40 +148,40 @@ export default {
|
|||
handleDragOnEnd(params) {
|
||||
sortableEnd();
|
||||
const { newIndex, oldIndex, from, to, item } = params;
|
||||
const { issueId, issueIid, issuePath } = item.dataset;
|
||||
const { itemId, itemIid, itemPath } = item.dataset;
|
||||
const { children } = to;
|
||||
let moveBeforeId;
|
||||
let moveAfterId;
|
||||
|
||||
const getIssueId = (el) => Number(el.dataset.issueId);
|
||||
const getItemId = (el) => Number(el.dataset.itemId);
|
||||
|
||||
// If issue is being moved within the same list
|
||||
// If item is being moved within the same list
|
||||
if (from === to) {
|
||||
if (newIndex > oldIndex && children.length > 1) {
|
||||
// If issue is being moved down we look for the issue that ends up before
|
||||
moveBeforeId = getIssueId(children[newIndex]);
|
||||
// If item is being moved down we look for the item that ends up before
|
||||
moveBeforeId = getItemId(children[newIndex]);
|
||||
} else if (newIndex < oldIndex && children.length > 1) {
|
||||
// If issue is being moved up we look for the issue that ends up after
|
||||
moveAfterId = getIssueId(children[newIndex]);
|
||||
// If item is being moved up we look for the item that ends up after
|
||||
moveAfterId = getItemId(children[newIndex]);
|
||||
} else {
|
||||
// If issue remains in the same list at the same position we do nothing
|
||||
// If item remains in the same list at the same position we do nothing
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// We look for the issue that ends up before the moved issue if it exists
|
||||
// We look for the item that ends up before the moved item if it exists
|
||||
if (children[newIndex - 1]) {
|
||||
moveBeforeId = getIssueId(children[newIndex - 1]);
|
||||
moveBeforeId = getItemId(children[newIndex - 1]);
|
||||
}
|
||||
// We look for the issue that ends up after the moved issue if it exists
|
||||
// We look for the item that ends up after the moved item if it exists
|
||||
if (children[newIndex]) {
|
||||
moveAfterId = getIssueId(children[newIndex]);
|
||||
moveAfterId = getItemId(children[newIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
this.moveIssue({
|
||||
issueId,
|
||||
issueIid,
|
||||
issuePath,
|
||||
this.moveItem({
|
||||
itemId,
|
||||
itemIid,
|
||||
itemPath,
|
||||
fromListId: from.dataset.listId,
|
||||
toListId: to.dataset.listId,
|
||||
moveBeforeId,
|
||||
|
@ -227,7 +226,7 @@ export default {
|
|||
:key="item.id"
|
||||
:index="index"
|
||||
:list="list"
|
||||
:issue="item"
|
||||
:item="item"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1">
|
||||
|
|
|
@ -325,17 +325,21 @@ export default {
|
|||
commit(types.RESET_ISSUES);
|
||||
},
|
||||
|
||||
moveItem: ({ dispatch }) => {
|
||||
dispatch('moveIssue');
|
||||
},
|
||||
|
||||
moveIssue: (
|
||||
{ state, commit },
|
||||
{ issueId, issueIid, issuePath, fromListId, toListId, moveBeforeId, moveAfterId },
|
||||
{ itemId, itemIid, itemPath, fromListId, toListId, moveBeforeId, moveAfterId },
|
||||
) => {
|
||||
const originalIssue = state.boardItems[issueId];
|
||||
const originalIssue = state.boardItems[itemId];
|
||||
const fromList = state.boardItemsByListId[fromListId];
|
||||
const originalIndex = fromList.indexOf(Number(issueId));
|
||||
const originalIndex = fromList.indexOf(Number(itemId));
|
||||
commit(types.MOVE_ISSUE, { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId });
|
||||
|
||||
const { boardId } = state;
|
||||
const [fullProjectPath] = issuePath.split(/[#]/);
|
||||
const [fullProjectPath] = itemPath.split(/[#]/);
|
||||
|
||||
gqlClient
|
||||
.mutate({
|
||||
|
@ -343,7 +347,7 @@ export default {
|
|||
variables: {
|
||||
projectPath: fullProjectPath,
|
||||
boardId: fullBoardId(boardId),
|
||||
iid: issueIid,
|
||||
iid: itemIid,
|
||||
fromListId: getIdFromGraphQLId(fromListId),
|
||||
toListId: getIdFromGraphQLId(toListId),
|
||||
moveBeforeId,
|
||||
|
@ -352,7 +356,7 @@ export default {
|
|||
})
|
||||
.then(({ data }) => {
|
||||
if (data?.issueMoveList?.errors.length) {
|
||||
commit(types.MOVE_ISSUE_FAILURE, { originalIssue, fromListId, toListId, originalIndex });
|
||||
throw new Error();
|
||||
} else {
|
||||
const issue = data.issueMoveList?.issue;
|
||||
commit(types.MOVE_ISSUE_SUCCESS, { issue });
|
||||
|
|
|
@ -2,7 +2,8 @@ import { pull, union } from 'lodash';
|
|||
import Vue from 'vue';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { s__ } from '~/locale';
|
||||
import { formatIssue, moveIssueListHelper } from '../boards_util';
|
||||
import { formatIssue, moveItemListHelper } from '../boards_util';
|
||||
import { issuableTypes } from '../constants';
|
||||
import * as mutationTypes from './mutation_types';
|
||||
|
||||
const notImplemented = () => {
|
||||
|
@ -10,13 +11,21 @@ const notImplemented = () => {
|
|||
throw new Error('Not implemented!');
|
||||
};
|
||||
|
||||
export const removeIssueFromList = ({ state, listId, issueId }) => {
|
||||
Vue.set(state.boardItemsByListId, listId, pull(state.boardItemsByListId[listId], issueId));
|
||||
const updateListItemsCount = ({ state, listId, value }) => {
|
||||
const list = state.boardLists[listId];
|
||||
Vue.set(state.boardLists, listId, { ...list, issuesCount: list.issuesCount - 1 });
|
||||
if (state.issuableType === issuableTypes.epic) {
|
||||
Vue.set(state.boardLists, listId, { ...list, epicsCount: list.epicsCount + value });
|
||||
} else {
|
||||
Vue.set(state.boardLists, listId, { ...list, issuesCount: list.issuesCount + value });
|
||||
}
|
||||
};
|
||||
|
||||
export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => {
|
||||
export const removeItemFromList = ({ state, listId, itemId }) => {
|
||||
Vue.set(state.boardItemsByListId, listId, pull(state.boardItemsByListId[listId], itemId));
|
||||
updateListItemsCount({ state, listId, value: -1 });
|
||||
};
|
||||
|
||||
export const addItemToList = ({ state, listId, itemId, moveBeforeId, moveAfterId, atIndex }) => {
|
||||
const listIssues = state.boardItemsByListId[listId];
|
||||
let newIndex = atIndex || 0;
|
||||
if (moveBeforeId) {
|
||||
|
@ -24,10 +33,9 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter
|
|||
} else if (moveAfterId) {
|
||||
newIndex = listIssues.indexOf(moveAfterId);
|
||||
}
|
||||
listIssues.splice(newIndex, 0, issueId);
|
||||
listIssues.splice(newIndex, 0, itemId);
|
||||
Vue.set(state.boardItemsByListId, listId, listIssues);
|
||||
const list = state.boardLists[listId];
|
||||
Vue.set(state.boardLists, listId, { ...list, issuesCount: list.issuesCount + 1 });
|
||||
updateListItemsCount({ state, listId, value: 1 });
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -182,11 +190,11 @@ export default {
|
|||
const fromList = state.boardLists[fromListId];
|
||||
const toList = state.boardLists[toListId];
|
||||
|
||||
const issue = moveIssueListHelper(originalIssue, fromList, toList);
|
||||
const issue = moveItemListHelper(originalIssue, fromList, toList);
|
||||
Vue.set(state.boardItems, issue.id, issue);
|
||||
|
||||
removeIssueFromList({ state, listId: fromListId, issueId: issue.id });
|
||||
addIssueToList({ state, listId: toListId, issueId: issue.id, moveBeforeId, moveAfterId });
|
||||
removeItemFromList({ state, listId: fromListId, itemId: issue.id });
|
||||
addItemToList({ state, listId: toListId, itemId: issue.id, moveBeforeId, moveAfterId });
|
||||
},
|
||||
|
||||
[mutationTypes.MOVE_ISSUE_SUCCESS]: (state, { issue }) => {
|
||||
|
@ -200,11 +208,11 @@ export default {
|
|||
) => {
|
||||
state.error = s__('Boards|An error occurred while moving the issue. Please try again.');
|
||||
Vue.set(state.boardItems, originalIssue.id, originalIssue);
|
||||
removeIssueFromList({ state, listId: toListId, issueId: originalIssue.id });
|
||||
addIssueToList({
|
||||
removeItemFromList({ state, listId: toListId, itemId: originalIssue.id });
|
||||
addItemToList({
|
||||
state,
|
||||
listId: fromListId,
|
||||
issueId: originalIssue.id,
|
||||
itemId: originalIssue.id,
|
||||
atIndex: originalIndex,
|
||||
});
|
||||
},
|
||||
|
@ -226,10 +234,10 @@ export default {
|
|||
},
|
||||
|
||||
[mutationTypes.ADD_ISSUE_TO_LIST]: (state, { list, issue, position }) => {
|
||||
addIssueToList({
|
||||
addItemToList({
|
||||
state,
|
||||
listId: list.id,
|
||||
issueId: issue.id,
|
||||
itemId: issue.id,
|
||||
atIndex: position,
|
||||
});
|
||||
Vue.set(state.boardItems, issue.id, issue);
|
||||
|
@ -237,11 +245,11 @@ export default {
|
|||
|
||||
[mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issueId }) => {
|
||||
state.error = s__('Boards|An error occurred while creating the issue. Please try again.');
|
||||
removeIssueFromList({ state, listId: list.id, issueId });
|
||||
removeItemFromList({ state, listId: list.id, itemId: issueId });
|
||||
},
|
||||
|
||||
[mutationTypes.REMOVE_ISSUE_FROM_LIST]: (state, { list, issue }) => {
|
||||
removeIssueFromList({ state, listId: list.id, issueId: issue.id });
|
||||
removeItemFromList({ state, listId: list.id, itemId: issue.id });
|
||||
Vue.delete(state.boardItems, issue.id);
|
||||
},
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ci-variable-table">
|
||||
<div class="ci-variable-table" data-testid="ci-variable-table">
|
||||
<gl-table
|
||||
:fields="fields"
|
||||
:items="variables"
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
<script>
|
||||
import { GlButton, GlModal, GlSprintf, GlIcon } from '@gitlab/ui';
|
||||
import { ISSUABLE_TYPE } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'CsvExportModal',
|
||||
components: {
|
||||
GlButton,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
GlIcon,
|
||||
},
|
||||
inject: {
|
||||
issuableType: {
|
||||
default: '',
|
||||
},
|
||||
issuableCount: {
|
||||
default: 0,
|
||||
},
|
||||
email: {
|
||||
default: '',
|
||||
},
|
||||
exportCsvPath: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
props: {
|
||||
modalId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
issuableName: this.issuableType === ISSUABLE_TYPE.issues ? 'issues' : 'merge requests',
|
||||
};
|
||||
},
|
||||
issueableType: ISSUABLE_TYPE,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-modal :modal-id="modalId" body-class="gl-p-0!">
|
||||
<template #modal-title>
|
||||
<gl-sprintf :message="__('Export %{name}')">
|
||||
<template #name>{{ issuableName }}</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
<div
|
||||
v-if="issuableCount > -1"
|
||||
data-testid="issuable-count-note"
|
||||
class="gl-justify-content-start gl-align-items-center gl-p-4 gl-border-b-solid gl-border-1 gl-border-gray-50"
|
||||
>
|
||||
<gl-icon name="check" class="gl-color-green-400" />
|
||||
<strong class="gl-m-3">
|
||||
<gl-sprintf
|
||||
v-if="issuableType === $options.issueableType.issues"
|
||||
:message="n__('1 issue selected', '%d issues selected', issuableCount)"
|
||||
>
|
||||
<template #issuableCount>{{ issuableCount }}</template>
|
||||
</gl-sprintf>
|
||||
<gl-sprintf
|
||||
v-else
|
||||
:message="n__('1 merge request selected', '%d merge request selected', issuableCount)"
|
||||
>
|
||||
<template #issuableCount>{{ issuableCount }}</template>
|
||||
</gl-sprintf>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="modal-text gl-px-4 gl-py-5">
|
||||
<gl-sprintf
|
||||
:message="
|
||||
__(
|
||||
`The CSV export will be created in the background. Once finished, it will be sent to %{strongStart}${email}%{strongEnd} in an attachment.`,
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #strong="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
<template #modal-footer>
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
:href="exportCsvPath"
|
||||
data-method="post"
|
||||
:data-qa-selector="`export_${issuableType}_button`"
|
||||
data-track-event="click_button"
|
||||
:data-track-label="`export_${issuableType}_csv`"
|
||||
>
|
||||
<gl-sprintf :message="__('Export %{name}')">
|
||||
<template #name>{{ issuableName }}</template>
|
||||
</gl-sprintf>
|
||||
</gl-button>
|
||||
</template>
|
||||
</gl-modal>
|
||||
</template>
|
|
@ -0,0 +1,86 @@
|
|||
<script>
|
||||
import {
|
||||
GlButtonGroup,
|
||||
GlButton,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlTooltipDirective,
|
||||
GlModalDirective,
|
||||
} from '@gitlab/ui';
|
||||
import CsvExportModal from './csv_export_modal.vue';
|
||||
import CsvImportModal from './csv_import_modal.vue';
|
||||
|
||||
export default {
|
||||
name: 'CsvImportExportButtons',
|
||||
components: {
|
||||
GlButtonGroup,
|
||||
GlButton,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
CsvExportModal,
|
||||
CsvImportModal,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
inject: {
|
||||
showExportButton: {
|
||||
default: false,
|
||||
},
|
||||
showImportButton: {
|
||||
default: false,
|
||||
},
|
||||
containerClass: {
|
||||
default: '',
|
||||
},
|
||||
canEdit: {
|
||||
default: false,
|
||||
},
|
||||
projectImportJiraPath: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
exportModalId() {
|
||||
return `${this.issuableType}-export-modal`;
|
||||
},
|
||||
importModalId() {
|
||||
return `${this.issuableType}-import-modal`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="containerClass">
|
||||
<gl-button-group>
|
||||
<gl-button
|
||||
v-if="showExportButton"
|
||||
v-gl-tooltip.hover="__('Export as CSV')"
|
||||
v-gl-modal="exportModalId"
|
||||
icon="export"
|
||||
data-testid="export-csv-button"
|
||||
/>
|
||||
<gl-dropdown
|
||||
v-if="showImportButton"
|
||||
v-gl-tooltip.hover="__('Import issues')"
|
||||
data-testid="import-csv-dropdown"
|
||||
icon="import"
|
||||
>
|
||||
<gl-dropdown-item v-gl-modal="importModalId" data-testid="import-csv-link">{{
|
||||
__('Import CSV')
|
||||
}}</gl-dropdown-item>
|
||||
<gl-dropdown-item
|
||||
v-if="canEdit"
|
||||
:href="projectImportJiraPath"
|
||||
data-qa-selector="import_from_jira_link"
|
||||
data-testid="import-from-jira-link"
|
||||
>{{ __('Import from Jira') }}</gl-dropdown-item
|
||||
>
|
||||
</gl-dropdown>
|
||||
</gl-button-group>
|
||||
<csv-export-modal v-if="showExportButton" :modal-id="exportModalId" />
|
||||
<csv-import-modal v-if="showImportButton" :modal-id="importModalId" />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,86 @@
|
|||
<script>
|
||||
import { GlModal, GlSprintf, GlFormGroup, GlButton } from '@gitlab/ui';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
import { ISSUABLE_TYPE } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'CsvImportModal',
|
||||
components: {
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
GlFormGroup,
|
||||
GlButton,
|
||||
},
|
||||
inject: {
|
||||
issuableType: {
|
||||
default: '',
|
||||
},
|
||||
exportCsvPath: {
|
||||
default: '',
|
||||
},
|
||||
importCsvIssuesPath: {
|
||||
default: '',
|
||||
},
|
||||
maxAttachmentSize: {
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
modalId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
issuableName: this.issuableType === ISSUABLE_TYPE.issues ? 'issues' : 'merge requests',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submitForm() {
|
||||
this.$refs.form.submit();
|
||||
},
|
||||
},
|
||||
csrf,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-modal :modal-id="modalId" :title="__('Import issues')">
|
||||
<form
|
||||
ref="form"
|
||||
:action="importCsvIssuesPath"
|
||||
enctype="multipart/form-data"
|
||||
method="post"
|
||||
data-testid="import-csv-form"
|
||||
>
|
||||
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
|
||||
<p>
|
||||
{{
|
||||
__(
|
||||
"Your issues will be imported in the background. Once finished, you'll get a confirmation email.",
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<gl-form-group :label="__('Upload CSV file')" label-for="file">
|
||||
<input id="file" type="file" name="file" accept=".csv,text/csv" />
|
||||
</gl-form-group>
|
||||
<p class="text-secondary">
|
||||
{{
|
||||
__(
|
||||
'It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected.',
|
||||
)
|
||||
}}
|
||||
<gl-sprintf :message="__('The maximum file size allowed is %{size}.')"
|
||||
><template #size>{{ maxAttachmentSize }}</template></gl-sprintf
|
||||
>
|
||||
</p>
|
||||
</form>
|
||||
<template #modal-footer>
|
||||
<gl-button category="primary" variant="confirm" @click="submitForm">{{
|
||||
__('Import issues')
|
||||
}}</gl-button>
|
||||
</template>
|
||||
</gl-modal>
|
||||
</template>
|
|
@ -1 +1,6 @@
|
|||
export const EVENT_ISSUABLE_VUE_APP_CHANGE = 'issuable_vue_app:change';
|
||||
|
||||
export const ISSUABLE_TYPE = {
|
||||
issues: 'issues',
|
||||
mergeRequests: 'merge-requests',
|
||||
};
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import Vue from 'vue';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import ImportExportButtons from './components/csv_import_export_buttons.vue';
|
||||
|
||||
export default () => {
|
||||
const el = document.querySelector('.js-csv-import-export-buttons');
|
||||
|
||||
if (!el) return null;
|
||||
|
||||
const {
|
||||
showExportButton,
|
||||
showImportButton,
|
||||
issuableType,
|
||||
issuableCount,
|
||||
email,
|
||||
exportCsvPath,
|
||||
importCsvIssuesPath,
|
||||
containerClass,
|
||||
canEdit,
|
||||
projectImportJiraPath,
|
||||
maxAttachmentSize,
|
||||
} = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
showExportButton: parseBoolean(showExportButton),
|
||||
showImportButton: parseBoolean(showImportButton),
|
||||
issuableType,
|
||||
issuableCount,
|
||||
email,
|
||||
exportCsvPath,
|
||||
importCsvIssuesPath,
|
||||
containerClass,
|
||||
canEdit: parseBoolean(canEdit),
|
||||
projectImportJiraPath,
|
||||
maxAttachmentSize,
|
||||
},
|
||||
render(h) {
|
||||
return h(ImportExportButtons);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -24,12 +24,21 @@ export default {
|
|||
default: '',
|
||||
},
|
||||
},
|
||||
model: {
|
||||
prop: 'visible',
|
||||
event: 'change',
|
||||
},
|
||||
props: {
|
||||
modalId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'custom-notifications-modal',
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -95,9 +104,11 @@ export default {
|
|||
<template>
|
||||
<gl-modal
|
||||
ref="modal"
|
||||
:visible="visible"
|
||||
:modal-id="modalId"
|
||||
:title="$options.i18n.customNotificationsModal.title"
|
||||
@show="onOpen"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
<script>
|
||||
import {
|
||||
GlButtonGroup,
|
||||
GlButton,
|
||||
GlDropdown,
|
||||
GlDropdownDivider,
|
||||
GlTooltipDirective,
|
||||
GlModalDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { GlDropdown, GlDropdownDivider, GlTooltipDirective } from '@gitlab/ui';
|
||||
import Api from '~/api';
|
||||
import { sprintf } from '~/locale';
|
||||
import { CUSTOM_LEVEL, i18n } from '../constants';
|
||||
|
@ -16,8 +9,6 @@ import NotificationsDropdownItem from './notifications_dropdown_item.vue';
|
|||
export default {
|
||||
name: 'NotificationsDropdown',
|
||||
components: {
|
||||
GlButtonGroup,
|
||||
GlButton,
|
||||
GlDropdown,
|
||||
GlDropdownDivider,
|
||||
NotificationsDropdownItem,
|
||||
|
@ -25,7 +16,6 @@ export default {
|
|||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
'gl-modal': GlModalDirective,
|
||||
},
|
||||
inject: {
|
||||
containerClass: {
|
||||
|
@ -57,6 +47,7 @@ export default {
|
|||
return {
|
||||
selectedNotificationLevel: this.initialNotificationLevel,
|
||||
isLoading: false,
|
||||
notificationsModalVisible: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -95,6 +86,11 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
openNotificationsModal() {
|
||||
if (this.isCustomNotification) {
|
||||
this.notificationsModalVisible = true;
|
||||
}
|
||||
},
|
||||
selectItem(level) {
|
||||
if (level !== this.selectedNotificationLevel) {
|
||||
this.updateNotificationLevel(level);
|
||||
|
@ -106,10 +102,7 @@ export default {
|
|||
try {
|
||||
await Api.updateNotificationSettings(this.projectId, this.groupId, { level });
|
||||
this.selectedNotificationLevel = level;
|
||||
|
||||
if (level === CUSTOM_LEVEL) {
|
||||
this.$refs.customNotificationsModal.open();
|
||||
}
|
||||
this.openNotificationsModal();
|
||||
} catch (error) {
|
||||
this.$toast.show(this.$options.i18n.updateNotificationLevelErrorMessage, { type: 'error' });
|
||||
} finally {
|
||||
|
@ -125,54 +118,16 @@ export default {
|
|||
|
||||
<template>
|
||||
<div :class="containerClass">
|
||||
<gl-button-group
|
||||
v-if="isCustomNotification"
|
||||
v-gl-tooltip="{ title: buttonTooltip }"
|
||||
data-testid="notification-button"
|
||||
:class="{ disabled: disabled }"
|
||||
:size="buttonSize"
|
||||
>
|
||||
<gl-button
|
||||
v-gl-modal="$options.modalId"
|
||||
:size="buttonSize"
|
||||
:icon="buttonIcon"
|
||||
:loading="isLoading"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template v-if="buttonText">{{ buttonText }}</template>
|
||||
</gl-button>
|
||||
<gl-dropdown :size="buttonSize" :disabled="disabled">
|
||||
<notifications-dropdown-item
|
||||
v-for="item in notificationLevels"
|
||||
:key="item.level"
|
||||
:level="item.level"
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
:notification-level="selectedNotificationLevel"
|
||||
@item-selected="selectItem"
|
||||
/>
|
||||
<gl-dropdown-divider />
|
||||
<notifications-dropdown-item
|
||||
:key="$options.customLevel"
|
||||
:level="$options.customLevel"
|
||||
:title="$options.i18n.notificationTitles.custom"
|
||||
:description="$options.i18n.notificationDescriptions.custom"
|
||||
:notification-level="selectedNotificationLevel"
|
||||
@item-selected="selectItem"
|
||||
/>
|
||||
</gl-dropdown>
|
||||
</gl-button-group>
|
||||
|
||||
<gl-dropdown
|
||||
v-else
|
||||
v-gl-tooltip="{ title: buttonTooltip }"
|
||||
data-testid="notification-button"
|
||||
:text="buttonText"
|
||||
data-testid="notification-dropdown"
|
||||
:size="buttonSize"
|
||||
:icon="buttonIcon"
|
||||
:loading="isLoading"
|
||||
:size="buttonSize"
|
||||
:disabled="disabled"
|
||||
:class="{ disabled: disabled }"
|
||||
:split="isCustomNotification"
|
||||
:text="buttonText"
|
||||
@click="openNotificationsModal"
|
||||
>
|
||||
<notifications-dropdown-item
|
||||
v-for="item in notificationLevels"
|
||||
|
@ -193,6 +148,6 @@ export default {
|
|||
@item-selected="selectItem"
|
||||
/>
|
||||
</gl-dropdown>
|
||||
<custom-notifications-modal ref="customNotificationsModal" :modal-id="$options.modalId" />
|
||||
<custom-notifications-modal v-model="notificationsModalVisible" :modal-id="$options.modalId" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
|
||||
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
||||
import initCsvImportExportButtons from '~/issuable/init_csv_import_export_buttons';
|
||||
import initIssuableByEmail from '~/issuable/init_issuable_by_email';
|
||||
import IssuableIndex from '~/issuable_index';
|
||||
import initIssuablesList from '~/issues_list';
|
||||
|
@ -26,3 +27,4 @@ new UsersSelect();
|
|||
initManualOrdering();
|
||||
initIssuablesList();
|
||||
initIssuableByEmail();
|
||||
initCsvImportExportButtons();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
|
||||
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
||||
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
|
||||
import initCsvImportExportButtons from '~/issuable/init_csv_import_export_buttons';
|
||||
import initIssuableByEmail from '~/issuable/init_issuable_by_email';
|
||||
import IssuableIndex from '~/issuable_index';
|
||||
import { FILTERED_SEARCH } from '~/pages/constants';
|
||||
|
@ -22,3 +23,4 @@ new UsersSelect(); // eslint-disable-line no-new
|
|||
new ShortcutsNavigation(); // eslint-disable-line no-new
|
||||
|
||||
initIssuableByEmail();
|
||||
initCsvImportExportButtons();
|
||||
|
|
|
@ -132,6 +132,8 @@ class Packages::Package < ApplicationRecord
|
|||
scope :order_project_path_desc, -> { joins(:project).reorder('projects.path DESC, id DESC') }
|
||||
scope :order_by_package_file, -> { joins(:package_files).order('packages_package_files.created_at ASC') }
|
||||
|
||||
after_commit :update_composer_cache, on: :destroy, if: -> { composer? }
|
||||
|
||||
def self.for_projects(projects)
|
||||
return none unless projects.any?
|
||||
|
||||
|
@ -232,6 +234,12 @@ class Packages::Package < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def update_composer_cache
|
||||
return unless composer?
|
||||
|
||||
::Packages::Composer::CacheUpdateWorker.perform_async(project_id, name, composer_metadatum.version_cache_sha) # rubocop:disable CodeReuse/Worker
|
||||
end
|
||||
|
||||
def composer_tag_version?
|
||||
composer? && !Gitlab::Regex.composer_dev_version_regex.match(version.to_s)
|
||||
end
|
||||
|
|
|
@ -17,6 +17,8 @@ module Packages
|
|||
})
|
||||
end
|
||||
|
||||
::Packages::Composer::CacheUpdateWorker.perform_async(created_package.project_id, created_package.name, nil)
|
||||
|
||||
created_package
|
||||
end
|
||||
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
- show_feed_buttons = local_assigns.fetch(:show_feed_buttons, true)
|
||||
- show_import_button = local_assigns.fetch(:show_import_button, true) && can?(current_user, :import_issues, @project)
|
||||
- show_export_button = local_assigns.fetch(:show_export_button, true)
|
||||
- issuable_type = 'issues'
|
||||
- can_edit = can?(current_user, :admin_project, @project)
|
||||
- notification_email = @current_user.present? ? @current_user.notification_email : nil
|
||||
|
||||
.nav-controls.issues-nav-controls
|
||||
- if show_feed_buttons
|
||||
= render 'shared/issuable/feed_buttons'
|
||||
|
||||
.btn-group
|
||||
- if show_export_button
|
||||
= render 'shared/issuable/csv_export/button', issuable_type: 'issues'
|
||||
|
||||
- if show_import_button
|
||||
= render 'projects/issues/import_csv/button'
|
||||
.js-csv-import-export-buttons{ data: { show_export_button: show_export_button.to_s, show_import_button: show_import_button.to_s, issuable_type: issuable_type, issuable_count: issuables_count_for_state(issuable_type.to_sym, params[:state]), email: notification_email, export_csv_path: export_csv_project_issues_path(@project, request.query_parameters), import_csv_issues_path: import_csv_namespace_project_issues_path, container_class: 'gl-mr-3', can_edit: can_edit.to_s, project_import_jira_path: project_import_jira_path(@project) } }
|
||||
|
||||
- if @can_bulk_update
|
||||
= button_tag _("Edit issues"), class: "gl-button btn btn-default gl-mr-3 js-bulk-update-toggle"
|
||||
|
@ -19,11 +17,6 @@
|
|||
= link_to _("New issue"), new_project_issue_path(@project,
|
||||
issue: { assignee_id: finder.assignee.try(:id),
|
||||
milestone_id: finder.milestones.first.try(:id) }),
|
||||
class: "gl-button btn btn-success",
|
||||
class: "gl-button btn btn-confirm",
|
||||
id: "new_issue_link"
|
||||
|
||||
- if show_export_button
|
||||
= render 'shared/issuable/csv_export/modal', issuable_type: 'issues'
|
||||
|
||||
- if show_import_button
|
||||
= render 'projects/issues/import_csv/modal'
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
.btn-group
|
||||
= render 'shared/issuable/csv_export/button', issuable_type: 'merge-requests'
|
||||
- issuable_type = 'merge-requests'
|
||||
- notification_email = @current_user.present? ? @current_user.notification_email : nil
|
||||
|
||||
.js-csv-import-export-buttons{ data: { show_export_button: "true", issuable_type: issuable_type, issuable_count: issuables_count_for_state(issuable_type.to_sym, params[:state]), email: notification_email, export_csv_path: export_csv_project_merge_requests_path(@project, request.query_parameters), container_class: 'gl-mr-3' } }
|
||||
|
||||
- if @can_bulk_update
|
||||
= button_tag "Edit merge requests", class: "gl-button btn btn-default gl-mr-3 js-bulk-update-toggle"
|
||||
- if merge_project
|
||||
= link_to new_merge_request_path, class: "gl-button btn btn-confirm", title: "New merge request" do
|
||||
New merge request
|
||||
|
||||
= render 'shared/issuable/csv_export/modal', issuable_type: 'merge_requests'
|
||||
|
|
|
@ -1908,6 +1908,14 @@
|
|||
:weight: 2
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: packages_composer_cache_update
|
||||
:feature_category: :package_registry
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: pages
|
||||
:feature_category: :pages
|
||||
:has_external_dependencies:
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Composer
|
||||
class CacheUpdateWorker
|
||||
include ApplicationWorker
|
||||
|
||||
feature_category :package_registry
|
||||
|
||||
idempotent!
|
||||
|
||||
def perform(project_id, package_name, last_page_sha)
|
||||
project = Project.find_by_id(project_id)
|
||||
|
||||
return unless project
|
||||
|
||||
Gitlab::Composer::Cache.new(project: project, name: package_name, last_page_sha: last_page_sha).execute
|
||||
rescue => e
|
||||
Gitlab::ErrorTracking.log_exception(e, project_id: project_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace import/export CSV modal with Vue component
|
||||
merge_request: 54214
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Composer cache update worker
|
||||
merge_request: 54551
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add Pages cache configuration settings
|
||||
merge_request: 55812
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add a confirmation prompt to lock and unlock path locks
|
||||
merge_request: 44849
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Simplify notifications dropdown
|
||||
merge_request: 55522
|
||||
author:
|
||||
type: changed
|
|
@ -240,6 +240,8 @@
|
|||
- 1
|
||||
- - package_repositories
|
||||
- 1
|
||||
- - packages_composer_cache_update
|
||||
- 1
|
||||
- - pages
|
||||
- 1
|
||||
- - pages_domain_ssl_renewal
|
||||
|
|
|
@ -6,9 +6,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Fast lookup of authorized SSH keys in the database
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1631) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.3.
|
||||
> - [Available in](https://gitlab.com/gitlab-org/gitlab/-/issues/3953) GitLab Community Edition 10.4.
|
||||
|
||||
NOTE:
|
||||
This document describes a drop-in replacement for the
|
||||
`authorized_keys` file. For normal (non-deploy key) users, consider using
|
||||
|
@ -219,8 +216,6 @@ the database. The following instructions can be used to build OpenSSH 7.5:
|
|||
|
||||
## SELinux support and limitations
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/2855) in GitLab 10.5.
|
||||
|
||||
GitLab supports `authorized_keys` database lookups with [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux).
|
||||
|
||||
Because the SELinux policy is static, GitLab doesn't support the ability to change
|
||||
|
|
|
@ -234,6 +234,12 @@ control over how the Pages daemon runs and serves content in your environment.
|
|||
| `external_https` | Configure Pages to bind to one or more secondary IP addresses, serving HTTPS requests. Multiple addresses can be given as an array, along with exact ports, for example `['1.2.3.4', '1.2.3.5:8063']`. Sets value for `listen_https`. |
|
||||
| `gitlab_client_http_timeout` | GitLab API HTTP client connection timeout in seconds (default: 10s). |
|
||||
| `gitlab_client_jwt_expiry` | JWT Token expiry time in seconds (default: 30s). |
|
||||
| `gitlab_cache_expiry` | The maximum time a domain's configuration is stored in the cache (default: 600s). |
|
||||
| `gitlab_cache_refresh` | The interval at which a domain's configuration is set to be due to refresh (default: 60s). |
|
||||
| `gitlab_cache_cleanup` | The interval at which expired items are removed from the cache (default: 60s). |
|
||||
| `gitlab_retrieval_timeout` | The maximum time to wait for a response from the GitLab API per request (default: 30s). |
|
||||
| `gitlab_retrieval_interval` | The interval to wait before retrying to resolve a domain's configuration via the GitLab API (default: 1s). |
|
||||
| `gitlab_retrieval_retries` | The maximum number of times to retry to resolve a domain's configuration via the API (default: 3). |
|
||||
| `domain_config_source` | Domain configuration source (default: `auto`) |
|
||||
| `gitlab_id` | The OAuth application public ID. Leave blank to automatically fill when Pages authenticates with GitLab. |
|
||||
| `gitlab_secret` | The OAuth application secret. Leave blank to automatically fill when Pages authenticates with GitLab. |
|
||||
|
@ -792,6 +798,44 @@ gitlab_pages['domain_config_source'] = "disk"
|
|||
For other common issues, see the [troubleshooting section](#failed-to-connect-to-the-internal-gitlab-api)
|
||||
or report an issue.
|
||||
|
||||
### GitLab API cache configuration
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-pages/-/issues/520) in GitLab 13.10.
|
||||
|
||||
API-based configuration uses a caching mechanism to improve performance and reliability of serving Pages.
|
||||
The cache behavior can be modified by changing the cache settings, however, the recommended values are set for you and should only be modified if needed.
|
||||
Incorrect configuration of these values may result in intermittent
|
||||
or persistent errors, or the Pages Daemon serving old content.
|
||||
|
||||
NOTE:
|
||||
Expiry, interval and timeout flags use [Golang's duration formatting](https://golang.org/pkg/time/#ParseDuration).
|
||||
A duration string is a possibly signed sequence of decimal numbers,
|
||||
each with optional fraction and a unit suffix, such as "300ms", "1.5h" or "2h45m".
|
||||
Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
|
||||
Examples:
|
||||
|
||||
- Increasing `gitlab_cache_expiry` will allow items to exist in the cache longer.
|
||||
This setting might be useful if the communication between GitLab Pages and GitLab Rails
|
||||
is not stable.
|
||||
|
||||
- Increasing `gitlab_cache_refresh` will reduce the frequency at which GitLab Pages
|
||||
requests a domain's configuration from GitLab Rails. This setting might be useful
|
||||
GitLab Pages generates too many requests to GitLab API and content does not change frequently.
|
||||
|
||||
- Decreasing `gitlab_cache_cleanup` will remove expired items from the cache more frequently,
|
||||
reducing the memory usage of your Pages node.
|
||||
|
||||
- Decreasing `gitlab_retrieval_timeout` allows you to stop the request to GitLab Rails
|
||||
more quickly. Increasing it will allow more time to receive a response from the API,
|
||||
useful in slow networking environments.
|
||||
|
||||
- Decreasing `gitlab_retrieval_interval` will make requests to the API more frequently,
|
||||
only when there is an error response from the API, for example a connection timeout.
|
||||
|
||||
- Decreasing `gitlab_retrieval_retries` will reduce the number of times a domain's
|
||||
configuration is tried to be resolved automatically before reporting an error.
|
||||
|
||||
## Using object storage
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5577) in GitLab 13.6.
|
||||
|
|
|
@ -1171,6 +1171,16 @@ msgid_plural "%d hours"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "1 issue selected"
|
||||
msgid_plural "%d issues selected"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "1 merge request selected"
|
||||
msgid_plural "%d merge request selected"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "1 merged merge request"
|
||||
msgid_plural "%{merge_requests} merged merge requests"
|
||||
msgstr[0] ""
|
||||
|
@ -3977,6 +3987,12 @@ msgid_plural "Are you sure you want to import %d repositories?"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Are you sure you want to lock %{path}?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to lock this directory?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to lose unsaved changes?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -4037,6 +4053,12 @@ msgstr ""
|
|||
msgid "Are you sure you want to unlock %{path_lock_path}?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to unlock %{path}?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to unlock this directory?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to unsubscribe from the %{type}: %{link_to_noteable_text}?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -4850,6 +4872,9 @@ msgstr ""
|
|||
msgid "Boards|An error occurred while generating lists. Please reload the page."
|
||||
msgstr ""
|
||||
|
||||
msgid "Boards|An error occurred while moving the epic. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Boards|An error occurred while moving the issue. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
@ -12424,6 +12449,9 @@ msgstr ""
|
|||
msgid "Export"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export %{name}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export %{requirementsCount} requirements?"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -183,7 +183,11 @@ module QA
|
|||
end
|
||||
|
||||
def fast_forward_possible?
|
||||
has_no_text?('Fast-forward merge is not possible')
|
||||
has_text?('Fast-forward merge without a merge commit')
|
||||
end
|
||||
|
||||
def fast_forward_not_possible?
|
||||
has_text?('Fast-forward merge is not possible')
|
||||
end
|
||||
|
||||
def has_file?(file_name)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Create', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/30226', type: :bug } do
|
||||
RSpec.describe 'Create', quarantine: { only: { subdomain: :staging }, issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/323990', type: :flaky } do
|
||||
describe 'Merge request rebasing' do
|
||||
it 'user rebases source branch of merge request', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1274' do
|
||||
Flow::Login.sign_in
|
||||
|
@ -34,7 +34,7 @@ module QA
|
|||
|
||||
Page::MergeRequest::Show.perform do |merge_request|
|
||||
expect(merge_request).to have_content('Needs rebasing')
|
||||
expect(merge_request).not_to be_fast_forward_possible
|
||||
expect(merge_request).to be_fast_forward_not_possible
|
||||
expect(merge_request).not_to have_merge_button
|
||||
|
||||
merge_request.rebase!
|
||||
|
|
|
@ -170,14 +170,14 @@ RSpec.describe 'Group show page' do
|
|||
it 'is enabled by default' do
|
||||
visit path
|
||||
|
||||
expect(page).to have_selector('[data-testid="notification-button"]:not(.disabled)')
|
||||
expect(page).to have_selector('[data-testid="notification-dropdown"] button:not(.disabled)')
|
||||
end
|
||||
|
||||
it 'is disabled if emails are disabled' do
|
||||
group.update_attribute(:emails_disabled, true)
|
||||
visit path
|
||||
|
||||
expect(page).to have_selector('[data-testid="notification-button"].disabled')
|
||||
expect(page).to have_selector('[data-testid="notification-dropdown"] .disabled')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Issues csv' do
|
||||
RSpec.describe 'Issues csv', :js do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:milestone) { create(:milestone, title: 'v1.0', project: project) }
|
||||
|
@ -17,7 +17,7 @@ RSpec.describe 'Issues csv' do
|
|||
def request_csv(params = {})
|
||||
visit project_issues_path(project, params)
|
||||
page.within('.nav-controls') do
|
||||
click_on 'Export as CSV'
|
||||
find('[data-testid="export-csv-button"]').click
|
||||
end
|
||||
click_on 'Export issues'
|
||||
end
|
||||
|
|
|
@ -14,11 +14,13 @@ RSpec.describe 'Merge Requests > Exports as CSV', :js do
|
|||
|
||||
subject { page.find('.nav-controls') }
|
||||
|
||||
it { is_expected.to have_button('Export as CSV') }
|
||||
it { is_expected.to have_selector '[data-testid="export-csv-button"]' }
|
||||
|
||||
context 'button is clicked' do
|
||||
before do
|
||||
click_button('Export as CSV')
|
||||
page.within('.nav-controls') do
|
||||
find('[data-testid="export-csv-button"]').click
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows a success message' do
|
||||
|
|
|
@ -15,17 +15,17 @@ RSpec.describe 'User visits the notifications tab', :js do
|
|||
it 'changes the project notifications setting' do
|
||||
expect(page).to have_content('Notifications')
|
||||
|
||||
first('[data-testid="notification-button"]').click
|
||||
first('[data-testid="notification-dropdown"]').click
|
||||
click_button('On mention')
|
||||
|
||||
expect(page).to have_selector('[data-testid="notification-button"]', text: 'On mention')
|
||||
expect(page).to have_selector('[data-testid="notification-dropdown"]', text: 'On mention')
|
||||
end
|
||||
|
||||
context 'when project emails are disabled' do
|
||||
let(:project) { create(:project, emails_disabled: true) }
|
||||
|
||||
it 'notification button is disabled' do
|
||||
expect(page).to have_selector('[data-testid="notification-button"].disabled')
|
||||
expect(page).to have_selector('[data-testid="notification-dropdown"] .disabled')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -57,7 +57,7 @@ RSpec.describe 'Project group variables', :js do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
expect(find('.js-ci-variable-row:nth-child(1) [data-label="Key"]').text).to eq(key1)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32,7 +32,7 @@ RSpec.describe 'Project variables', :js do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ RSpec.describe 'Projects > Show > User manages notifications', :js do
|
|||
end
|
||||
|
||||
def click_notifications_button
|
||||
first('[data-testid="notification-button"]').click
|
||||
first('[data-testid="notification-dropdown"]').click
|
||||
end
|
||||
|
||||
it 'changes the notification setting' do
|
||||
|
@ -22,7 +22,7 @@ RSpec.describe 'Projects > Show > User manages notifications', :js do
|
|||
|
||||
click_notifications_button
|
||||
|
||||
page.within first('[data-testid="notification-button"]') do
|
||||
page.within first('[data-testid="notification-dropdown"]') do
|
||||
expect(page.find('.gl-new-dropdown-item.is-active')).to have_content('On mention')
|
||||
expect(page).to have_css('[data-testid="notifications-icon"]')
|
||||
end
|
||||
|
@ -33,7 +33,7 @@ RSpec.describe 'Projects > Show > User manages notifications', :js do
|
|||
click_notifications_button
|
||||
click_button 'Disabled'
|
||||
|
||||
page.within first('[data-testid="notification-button"]') do
|
||||
page.within first('[data-testid="notification-dropdown"]') do
|
||||
expect(page).to have_css('[data-testid="notifications-off-icon"]')
|
||||
end
|
||||
end
|
||||
|
@ -80,7 +80,7 @@ RSpec.describe 'Projects > Show > User manages notifications', :js do
|
|||
|
||||
it 'is disabled' do
|
||||
visit project_path(project)
|
||||
expect(page).to have_selector('[data-testid="notification-button"].disabled', visible: true)
|
||||
expect(page).to have_selector('[data-testid="notification-dropdown"] .disabled', visible: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -125,7 +125,7 @@ describe('Board list component', () => {
|
|||
});
|
||||
|
||||
it('sets data attribute with issue id', () => {
|
||||
expect(wrapper.find('.board-card').attributes('data-issue-id')).toBe('1');
|
||||
expect(wrapper.find('.board-card').attributes('data-item-id')).toBe('1');
|
||||
});
|
||||
|
||||
it('shows new issue form', async () => {
|
||||
|
@ -258,7 +258,7 @@ describe('Board list component', () => {
|
|||
|
||||
describe('handleDragOnEnd', () => {
|
||||
it('removes class `is-dragging` from document body', () => {
|
||||
jest.spyOn(wrapper.vm, 'moveIssue').mockImplementation(() => {});
|
||||
jest.spyOn(wrapper.vm, 'moveItem').mockImplementation(() => {});
|
||||
document.body.classList.add('is-dragging');
|
||||
|
||||
findByTestId('tree-root-wrapper').vm.$emit('end', {
|
||||
|
@ -266,9 +266,9 @@ describe('Board list component', () => {
|
|||
newIndex: 0,
|
||||
item: {
|
||||
dataset: {
|
||||
issueId: mockIssues[0].id,
|
||||
issueIid: mockIssues[0].iid,
|
||||
issuePath: mockIssues[0].referencePath,
|
||||
itemId: mockIssues[0].id,
|
||||
itemIid: mockIssues[0].iid,
|
||||
itemPath: mockIssues[0].referencePath,
|
||||
},
|
||||
},
|
||||
to: { children: [], dataset: { listId: 'gid://gitlab/List/1' } },
|
||||
|
|
|
@ -6,7 +6,7 @@ import BoardCardInner from '~/boards/components/board_card_inner.vue';
|
|||
import { inactiveId } from '~/boards/constants';
|
||||
import { mockLabelList, mockIssue } from '../mock_data';
|
||||
|
||||
describe('Board card layout', () => {
|
||||
describe('Board card', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
let mockActions;
|
||||
|
@ -44,7 +44,7 @@ describe('Board card layout', () => {
|
|||
store,
|
||||
propsData: {
|
||||
list: mockLabelList,
|
||||
issue: mockIssue,
|
||||
item: mockIssue,
|
||||
disabled: false,
|
||||
index: 0,
|
||||
...propsData,
|
||||
|
@ -113,7 +113,7 @@ describe('Board card layout', () => {
|
|||
expect(wrapper.classes()).not.toContain('is-active');
|
||||
});
|
||||
|
||||
describe('when mouseup event is called on the issue card', () => {
|
||||
describe('when mouseup event is called on the card', () => {
|
||||
beforeEach(() => {
|
||||
createStore({ isSwimlanesOn });
|
||||
mountComponent();
|
||||
|
|
|
@ -637,6 +637,15 @@ describe('resetIssues', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('moveItem', () => {
|
||||
it('should dispatch moveIssue action', () => {
|
||||
testAction({
|
||||
action: actions.moveItem,
|
||||
expectedActions: [{ type: 'moveIssue' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveIssue', () => {
|
||||
const listIssues = {
|
||||
'gid://gitlab/List/1': [436, 437],
|
||||
|
@ -671,9 +680,9 @@ describe('moveIssue', () => {
|
|||
testAction(
|
||||
actions.moveIssue,
|
||||
{
|
||||
issueId: '436',
|
||||
issueIid: mockIssue.iid,
|
||||
issuePath: mockIssue.referencePath,
|
||||
itemId: '436',
|
||||
itemIid: mockIssue.iid,
|
||||
itemPath: mockIssue.referencePath,
|
||||
fromListId: 'gid://gitlab/List/1',
|
||||
toListId: 'gid://gitlab/List/2',
|
||||
},
|
||||
|
@ -722,9 +731,9 @@ describe('moveIssue', () => {
|
|||
actions.moveIssue(
|
||||
{ state, commit: () => {} },
|
||||
{
|
||||
issueId: mockIssue.id,
|
||||
issueIid: mockIssue.iid,
|
||||
issuePath: mockIssue.referencePath,
|
||||
itemId: mockIssue.id,
|
||||
itemIid: mockIssue.iid,
|
||||
itemPath: mockIssue.referencePath,
|
||||
fromListId: 'gid://gitlab/List/1',
|
||||
toListId: 'gid://gitlab/List/2',
|
||||
},
|
||||
|
@ -746,9 +755,9 @@ describe('moveIssue', () => {
|
|||
testAction(
|
||||
actions.moveIssue,
|
||||
{
|
||||
issueId: '436',
|
||||
issueIid: mockIssue.iid,
|
||||
issuePath: mockIssue.referencePath,
|
||||
itemId: '436',
|
||||
itemIid: mockIssue.iid,
|
||||
itemPath: mockIssue.referencePath,
|
||||
fromListId: 'gid://gitlab/List/1',
|
||||
toListId: 'gid://gitlab/List/2',
|
||||
},
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import { GlModal, GlIcon, GlButton } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import CsvExportModal from '~/issuable/components/csv_export_modal.vue';
|
||||
|
||||
describe('CsvExportModal', () => {
|
||||
let wrapper;
|
||||
|
||||
function createComponent(options = {}) {
|
||||
const { injectedProperties = {}, props = {} } = options;
|
||||
return extendedWrapper(
|
||||
mount(CsvExportModal, {
|
||||
propsData: {
|
||||
modalId: 'csv-export-modal',
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
issuableType: 'issues',
|
||||
...injectedProperties,
|
||||
},
|
||||
stubs: {
|
||||
GlModal: stubComponent(GlModal, {
|
||||
template:
|
||||
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findIcon = () => wrapper.findComponent(GlIcon);
|
||||
const findButton = () => wrapper.findComponent(GlButton);
|
||||
|
||||
describe('template', () => {
|
||||
describe.each`
|
||||
issuableType | modalTitle
|
||||
${'issues'} | ${'Export issues'}
|
||||
${'merge-requests'} | ${'Export merge requests'}
|
||||
`('with the issuableType "$issuableType"', ({ issuableType, modalTitle }) => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ injectedProperties: { issuableType } });
|
||||
});
|
||||
|
||||
it('displays the modal title "$modalTitle"', () => {
|
||||
expect(findModal().text()).toContain(modalTitle);
|
||||
});
|
||||
|
||||
it('displays the button with title "$modalTitle"', () => {
|
||||
expect(findButton().text()).toBe(modalTitle);
|
||||
});
|
||||
});
|
||||
|
||||
describe('issuable count info text', () => {
|
||||
it('displays the info text when issuableCount is > -1', () => {
|
||||
wrapper = createComponent({ injectedProperties: { issuableCount: 10 } });
|
||||
expect(wrapper.findByTestId('issuable-count-note').exists()).toBe(true);
|
||||
expect(wrapper.findByTestId('issuable-count-note').text()).toContain('10 issues selected');
|
||||
expect(findIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("doesn't display the info text when issuableCount is -1", () => {
|
||||
wrapper = createComponent({ injectedProperties: { issuableCount: -1 } });
|
||||
expect(wrapper.findByTestId('issuable-count-note').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('email info text', () => {
|
||||
it('displays the proper email', () => {
|
||||
const email = 'admin@example.com';
|
||||
wrapper = createComponent({ injectedProperties: { email } });
|
||||
expect(findModal().text()).toContain(
|
||||
`The CSV export will be created in the background. Once finished, it will be sent to ${email} in an attachment.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('primary button', () => {
|
||||
it('passes the exportCsvPath to the button', () => {
|
||||
const exportCsvPath = '/gitlab-org/gitlab-test/-/issues/export_csv';
|
||||
wrapper = createComponent({ injectedProperties: { exportCsvPath } });
|
||||
expect(findButton().attributes('href')).toBe(exportCsvPath);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,161 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import CsvExportModal from '~/issuable/components/csv_export_modal.vue';
|
||||
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
|
||||
import CsvImportModal from '~/issuable/components/csv_import_modal.vue';
|
||||
|
||||
describe('CsvImportExportButtons', () => {
|
||||
let wrapper;
|
||||
let glModalDirective;
|
||||
|
||||
function createComponent(injectedProperties = {}) {
|
||||
glModalDirective = jest.fn();
|
||||
return extendedWrapper(
|
||||
shallowMount(CsvImportExportButtons, {
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
glModal: {
|
||||
bind(_, { value }) {
|
||||
glModalDirective(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
provide: {
|
||||
...injectedProperties,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findExportCsvButton = () => wrapper.findByTestId('export-csv-button');
|
||||
const findImportDropdown = () => wrapper.findByTestId('import-csv-dropdown');
|
||||
const findImportCsvButton = () => wrapper.findByTestId('import-csv-dropdown');
|
||||
const findImportFromJiraLink = () => wrapper.findByTestId('import-from-jira-link');
|
||||
const findExportCsvModal = () => wrapper.findComponent(CsvExportModal);
|
||||
const findImportCsvModal = () => wrapper.findComponent(CsvImportModal);
|
||||
|
||||
describe('template', () => {
|
||||
describe('when the showExportButton=true', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ showExportButton: true });
|
||||
});
|
||||
|
||||
it('displays the export button', () => {
|
||||
expect(findExportCsvButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('export button has a tooltip', () => {
|
||||
const tooltip = getBinding(findExportCsvButton().element, 'gl-tooltip');
|
||||
|
||||
expect(tooltip).toBeDefined();
|
||||
expect(tooltip.value).toBe('Export as CSV');
|
||||
});
|
||||
|
||||
it('renders the export modal', () => {
|
||||
expect(findExportCsvModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('opens the export modal', () => {
|
||||
findExportCsvButton().trigger('click');
|
||||
|
||||
expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.exportModalId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the showExportButton=false', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ showExportButton: false });
|
||||
});
|
||||
|
||||
it('does not display the export button', () => {
|
||||
expect(findExportCsvButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render the export modal', () => {
|
||||
expect(findExportCsvModal().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the showImportButton=true', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ showImportButton: true });
|
||||
});
|
||||
|
||||
it('displays the import dropdown', () => {
|
||||
expect(findImportDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the import button', () => {
|
||||
expect(findImportCsvButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('import button has a tooltip', () => {
|
||||
const tooltip = getBinding(findImportDropdown().element, 'gl-tooltip');
|
||||
|
||||
expect(tooltip).toBeDefined();
|
||||
expect(tooltip.value).toBe('Import issues');
|
||||
});
|
||||
|
||||
it('renders the import modal', () => {
|
||||
expect(findImportCsvModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('opens the import modal', () => {
|
||||
findImportCsvButton().trigger('click');
|
||||
|
||||
expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.importModalId);
|
||||
});
|
||||
|
||||
describe('import from jira link', () => {
|
||||
const projectImportJiraPath = 'gitlab-org/gitlab-test/-/import/jira';
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({
|
||||
showImportButton: true,
|
||||
canEdit: true,
|
||||
projectImportJiraPath,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when canEdit=true', () => {
|
||||
it('renders the import dropdown item', () => {
|
||||
expect(findImportFromJiraLink().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('passes the proper path to the link', () => {
|
||||
expect(findImportFromJiraLink().attributes('href')).toBe(projectImportJiraPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when canEdit=false', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ showImportButton: true, canEdit: false });
|
||||
});
|
||||
|
||||
it('does not render the import dropdown item', () => {
|
||||
expect(findImportFromJiraLink().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the showImportButton=false', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ showImportButton: false });
|
||||
});
|
||||
|
||||
it('does not display the import dropdown', () => {
|
||||
expect(findImportDropdown().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render the import modal', () => {
|
||||
expect(findImportCsvModal().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
import { GlModal } from '@gitlab/ui';
|
||||
import { getByRole, getByLabelText } from '@testing-library/dom';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import CsvImportModal from '~/issuable/components/csv_import_modal.vue';
|
||||
|
||||
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
|
||||
|
||||
describe('CsvImportModal', () => {
|
||||
let wrapper;
|
||||
let formSubmitSpy;
|
||||
|
||||
function createComponent(options = {}) {
|
||||
const { injectedProperties = {}, props = {} } = options;
|
||||
return extendedWrapper(
|
||||
mount(CsvImportModal, {
|
||||
propsData: {
|
||||
modalId: 'csv-import-modal',
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
issuableType: 'issues',
|
||||
...injectedProperties,
|
||||
},
|
||||
stubs: {
|
||||
GlModal: stubComponent(GlModal, {
|
||||
template: '<div><slot></slot><slot name="modal-footer"></slot></div>',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
formSubmitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findPrimaryButton = () => getByRole(wrapper.element, 'button', { name: 'Import issues' });
|
||||
const findForm = () => wrapper.findByTestId('import-csv-form');
|
||||
const findFileInput = () => getByLabelText(wrapper.element, 'Upload CSV file');
|
||||
const findAuthenticityToken = () => new FormData(findForm().element).get('authenticity_token');
|
||||
|
||||
describe('template', () => {
|
||||
it('displays modal title', () => {
|
||||
wrapper = createComponent();
|
||||
expect(findModal().text()).toContain('Import issues');
|
||||
});
|
||||
|
||||
it('displays a note about the maximum allowed file size', () => {
|
||||
const maxAttachmentSize = 500;
|
||||
wrapper = createComponent({ injectedProperties: { maxAttachmentSize } });
|
||||
expect(findModal().text()).toContain(`The maximum file size allowed is ${maxAttachmentSize}`);
|
||||
});
|
||||
|
||||
describe('form', () => {
|
||||
const importCsvIssuesPath = 'gitlab-org/gitlab-test/-/issues/import_csv';
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ injectedProperties: { importCsvIssuesPath } });
|
||||
});
|
||||
|
||||
it('displays the form with the correct action and inputs', () => {
|
||||
expect(findForm().exists()).toBe(true);
|
||||
expect(findForm().attributes('action')).toBe(importCsvIssuesPath);
|
||||
expect(findAuthenticityToken()).toBe('mock-csrf-token');
|
||||
expect(findFileInput()).toExist();
|
||||
});
|
||||
|
||||
it('displays the correct primary button action text', () => {
|
||||
expect(findPrimaryButton()).toExist();
|
||||
});
|
||||
|
||||
it('submits the form when the primary action is clicked', async () => {
|
||||
findPrimaryButton().click();
|
||||
|
||||
expect(formSubmitSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
import { GlButtonGroup, GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import axios from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
@ -15,14 +15,10 @@ const mockToastShow = jest.fn();
|
|||
describe('NotificationsDropdown', () => {
|
||||
let wrapper;
|
||||
let mockAxios;
|
||||
let glModalDirective;
|
||||
|
||||
function createComponent(injectedProperties = {}) {
|
||||
glModalDirective = jest.fn();
|
||||
|
||||
return shallowMount(NotificationsDropdown, {
|
||||
stubs: {
|
||||
GlButtonGroup,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
NotificationsDropdownItem,
|
||||
|
@ -30,11 +26,6 @@ describe('NotificationsDropdown', () => {
|
|||
},
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
glModal: {
|
||||
bind(_, { value }) {
|
||||
glModalDirective(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
provide: {
|
||||
dropdownItems: mockDropdownItems,
|
||||
|
@ -49,13 +40,12 @@ describe('NotificationsDropdown', () => {
|
|||
});
|
||||
}
|
||||
|
||||
const findButtonGroup = () => wrapper.find(GlButtonGroup);
|
||||
const findButton = () => wrapper.find(GlButton);
|
||||
const findDropdown = () => wrapper.find(GlDropdown);
|
||||
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
|
||||
const findAllNotificationsDropdownItems = () => wrapper.findAll(NotificationsDropdownItem);
|
||||
const findDropdownItemAt = (index) =>
|
||||
findAllNotificationsDropdownItems().at(index).find(GlDropdownItem);
|
||||
const findNotificationsModal = () => wrapper.find(CustomNotificationsModal);
|
||||
|
||||
const clickDropdownItemAt = async (index) => {
|
||||
const dropdownItem = findDropdownItemAt(index);
|
||||
|
@ -83,8 +73,8 @@ describe('NotificationsDropdown', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('renders a button group', () => {
|
||||
expect(findButtonGroup().exists()).toBe(true);
|
||||
it('renders split dropdown', () => {
|
||||
expect(findDropdown().props().split).toBe(true);
|
||||
});
|
||||
|
||||
it('shows the button text when showLabel is true', () => {
|
||||
|
@ -93,7 +83,7 @@ describe('NotificationsDropdown', () => {
|
|||
showLabel: true,
|
||||
});
|
||||
|
||||
expect(findButton().text()).toBe('Custom');
|
||||
expect(findDropdown().props().text).toBe('Custom');
|
||||
});
|
||||
|
||||
it("doesn't show the button text when showLabel is false", () => {
|
||||
|
@ -102,7 +92,7 @@ describe('NotificationsDropdown', () => {
|
|||
showLabel: false,
|
||||
});
|
||||
|
||||
expect(findButton().text()).toBe('');
|
||||
expect(findDropdown().props().text).toBe(null);
|
||||
});
|
||||
|
||||
it('opens the modal when the user clicks the button', async () => {
|
||||
|
@ -113,9 +103,9 @@ describe('NotificationsDropdown', () => {
|
|||
initialNotificationLevel: 'custom',
|
||||
});
|
||||
|
||||
findButton().vm.$emit('click');
|
||||
await findDropdown().vm.$emit('click');
|
||||
|
||||
expect(glModalDirective).toHaveBeenCalled();
|
||||
expect(findNotificationsModal().props().visible).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -126,8 +116,8 @@ describe('NotificationsDropdown', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not render a button group', () => {
|
||||
expect(findButtonGroup().exists()).toBe(false);
|
||||
it('renders unified dropdown', () => {
|
||||
expect(findDropdown().props().split).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the button text when showLabel is true', () => {
|
||||
|
@ -162,7 +152,7 @@ describe('NotificationsDropdown', () => {
|
|||
initialNotificationLevel: level,
|
||||
});
|
||||
|
||||
const tooltipElement = findByTestId('notification-button');
|
||||
const tooltipElement = findByTestId('notification-dropdown');
|
||||
const tooltip = getBinding(tooltipElement.element, 'gl-tooltip');
|
||||
|
||||
expect(tooltip.value.title).toBe(`${tooltipTitlePrefix} - ${title}`);
|
||||
|
@ -264,11 +254,9 @@ describe('NotificationsDropdown', () => {
|
|||
mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {});
|
||||
wrapper = createComponent();
|
||||
|
||||
const mockModalShow = jest.spyOn(wrapper.vm.$refs.customNotificationsModal, 'open');
|
||||
|
||||
await clickDropdownItemAt(5);
|
||||
|
||||
expect(mockModalShow).toHaveBeenCalled();
|
||||
expect(findNotificationsModal().props().visible).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -854,4 +854,22 @@ RSpec.describe Packages::Package, type: :model do
|
|||
it_behaves_like 'not enqueuing a sync worker job'
|
||||
end
|
||||
end
|
||||
|
||||
context 'destroying a composer package' do
|
||||
let_it_be(:package_name) { 'composer-package-name' }
|
||||
let_it_be(:json) { { 'name' => package_name } }
|
||||
let_it_be(:project) { create(:project, :custom_repo, files: { 'composer.json' => json.to_json } ) }
|
||||
let!(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) }
|
||||
|
||||
before do
|
||||
Gitlab::Composer::Cache.new(project: project, name: package_name).execute
|
||||
package.composer_metadatum.reload
|
||||
end
|
||||
|
||||
it 'schedule the update job' do
|
||||
expect(::Packages::Composer::CacheUpdateWorker).to receive(:perform_async).with(project.id, package_name, package.composer_metadatum.version_cache_sha)
|
||||
|
||||
package.destroy!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,6 +28,8 @@ RSpec.describe Packages::Composer::CreatePackageService do
|
|||
let(:branch) { project.repository.find_branch('master') }
|
||||
|
||||
it 'creates the package' do
|
||||
expect(::Packages::Composer::CacheUpdateWorker).to receive(:perform_async).with(project.id, package_name, nil)
|
||||
|
||||
expect { subject }
|
||||
.to change { Packages::Package.composer.count }.by(1)
|
||||
.and change { Packages::Composer::Metadatum.count }.by(1)
|
||||
|
@ -54,6 +56,8 @@ RSpec.describe Packages::Composer::CreatePackageService do
|
|||
end
|
||||
|
||||
it 'creates the package' do
|
||||
expect(::Packages::Composer::CacheUpdateWorker).to receive(:perform_async).with(project.id, package_name, nil)
|
||||
|
||||
expect { subject }
|
||||
.to change { Packages::Package.composer.count }.by(1)
|
||||
.and change { Packages::Composer::Metadatum.count }.by(1)
|
||||
|
@ -80,6 +84,8 @@ RSpec.describe Packages::Composer::CreatePackageService do
|
|||
end
|
||||
|
||||
it 'does not create a new package' do
|
||||
expect(::Packages::Composer::CacheUpdateWorker).to receive(:perform_async).with(project.id, package_name, nil)
|
||||
|
||||
expect { subject }
|
||||
.to change { Packages::Package.composer.count }.by(0)
|
||||
.and change { Packages::Composer::Metadatum.count }.by(0)
|
||||
|
@ -101,6 +107,8 @@ RSpec.describe Packages::Composer::CreatePackageService do
|
|||
let!(:other_package) { create(:package, name: package_name, version: 'dev-master', project: other_project) }
|
||||
|
||||
it 'creates the package' do
|
||||
expect(::Packages::Composer::CacheUpdateWorker).to receive(:perform_async).with(project.id, package_name, nil)
|
||||
|
||||
expect { subject }
|
||||
.to change { Packages::Package.composer.count }.by(1)
|
||||
.and change { Packages::Composer::Metadatum.count }.by(1)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
RSpec.shared_examples 'variable list' do
|
||||
it 'shows a list of variables' do
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key)
|
||||
end
|
||||
end
|
||||
|
@ -16,7 +16,7 @@ RSpec.shared_examples 'variable list' do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key')
|
||||
end
|
||||
end
|
||||
|
@ -30,7 +30,7 @@ RSpec.shared_examples 'variable list' do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key')
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Protected"] svg[data-testid="mobile-issue-close-icon"]')).to be_present
|
||||
end
|
||||
|
@ -45,14 +45,14 @@ RSpec.shared_examples 'variable list' do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key')
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="close-icon"]')).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
it 'reveals and hides variables' do
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key)
|
||||
expect(page).to have_content('*' * 17)
|
||||
|
||||
|
@ -72,7 +72,7 @@ RSpec.shared_examples 'variable list' do
|
|||
it 'deletes a variable' do
|
||||
expect(page).to have_selector('.js-ci-variable-row', count: 1)
|
||||
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
click_button('Edit')
|
||||
end
|
||||
|
||||
|
@ -86,7 +86,7 @@ RSpec.shared_examples 'variable list' do
|
|||
end
|
||||
|
||||
it 'edits a variable' do
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
click_button('Edit')
|
||||
end
|
||||
|
||||
|
@ -102,7 +102,7 @@ RSpec.shared_examples 'variable list' do
|
|||
end
|
||||
|
||||
it 'edits a variable to be unmasked' do
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
click_button('Edit')
|
||||
end
|
||||
|
||||
|
@ -115,13 +115,13 @@ RSpec.shared_examples 'variable list' do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="close-icon"]')).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
it 'edits a variable to be masked' do
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
click_button('Edit')
|
||||
end
|
||||
|
||||
|
@ -133,7 +133,7 @@ RSpec.shared_examples 'variable list' do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
click_button('Edit')
|
||||
end
|
||||
|
||||
|
@ -143,7 +143,7 @@ RSpec.shared_examples 'variable list' do
|
|||
click_button('Update variable')
|
||||
end
|
||||
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="mobile-issue-close-icon"]')).to be_present
|
||||
end
|
||||
end
|
||||
|
@ -211,7 +211,7 @@ RSpec.shared_examples 'variable list' do
|
|||
expect(page).to have_selector('.js-ci-variable-row', count: 3)
|
||||
|
||||
# Remove the `akey` variable
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
page.within('.js-ci-variable-row:first-child') do
|
||||
click_button('Edit')
|
||||
end
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Packages::Composer::CacheUpdateWorker, type: :worker do
|
||||
describe '#perform' do
|
||||
let_it_be(:package_name) { 'sample-project' }
|
||||
let_it_be(:json) { { 'name' => package_name } }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, :custom_repo, files: { 'composer.json' => json.to_json }, group: group) }
|
||||
let(:last_sha) { nil }
|
||||
let!(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) }
|
||||
let(:job_args) { [project.id, package_name, last_sha] }
|
||||
|
||||
subject { described_class.new.perform(*job_args) }
|
||||
|
||||
before do
|
||||
stub_composer_cache_object_storage
|
||||
end
|
||||
|
||||
include_examples 'an idempotent worker' do
|
||||
context 'creating a package' do
|
||||
it 'updates the cache' do
|
||||
expect { subject }.to change { Packages::Composer::CacheFile.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'deleting a package' do
|
||||
let!(:last_sha) do
|
||||
Gitlab::Composer::Cache.new(project: project, name: package_name).execute
|
||||
package.reload.composer_metadatum.version_cache_sha
|
||||
end
|
||||
|
||||
before do
|
||||
package.destroy!
|
||||
end
|
||||
|
||||
it 'marks the file for deletion' do
|
||||
expect { subject }.not_to change { Packages::Composer::CacheFile.count }
|
||||
|
||||
cache_file = Packages::Composer::CacheFile.last
|
||||
|
||||
expect(cache_file.reload.delete_at).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue