Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
130e0444c6
commit
3d233a67cf
49 changed files with 385 additions and 906 deletions
|
@ -8,6 +8,7 @@ import defaultSortableConfig from '~/sortable/sortable_config';
|
|||
import Tracking from '~/tracking';
|
||||
import { toggleFormEventPrefix, DraggableItemTypes } from '../constants';
|
||||
import eventHub from '../eventhub';
|
||||
import listQuery from '../graphql/board_lists_deferred.query.graphql';
|
||||
import BoardCard from './board_card.vue';
|
||||
import BoardNewIssue from './board_new_issue.vue';
|
||||
|
||||
|
@ -50,11 +51,22 @@ export default {
|
|||
showEpicForm: false,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
boardList: {
|
||||
query: listQuery,
|
||||
variables() {
|
||||
return {
|
||||
id: this.list.id,
|
||||
filters: this.filterParams,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['pageInfoByListId', 'listsFlags']),
|
||||
...mapState(['pageInfoByListId', 'listsFlags', 'filterParams']),
|
||||
...mapGetters(['isEpicBoard']),
|
||||
listItemsCount() {
|
||||
return this.isEpicBoard ? this.list.epicsCount : this.list.issuesCount;
|
||||
return this.isEpicBoard ? this.list.epicsCount : this.boardList?.issuesCount;
|
||||
},
|
||||
paginatedIssueText() {
|
||||
return sprintf(__('Showing %{pageSize} of %{total} %{issuableType}'), {
|
||||
|
|
|
@ -20,6 +20,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|||
import AccessorUtilities from '../../lib/utils/accessor';
|
||||
import { inactiveId, LIST, ListType, toggleFormEventPrefix } from '../constants';
|
||||
import eventHub from '../eventhub';
|
||||
import listQuery from '../graphql/board_lists_deferred.query.graphql';
|
||||
import ItemCount from './item_count.vue';
|
||||
|
||||
export default {
|
||||
|
@ -74,7 +75,7 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['activeId']),
|
||||
...mapState(['activeId', 'filterParams']),
|
||||
...mapGetters(['isEpicBoard', 'isSwimlanesOn']),
|
||||
isLoggedIn() {
|
||||
return Boolean(this.currentUserId);
|
||||
|
@ -119,14 +120,11 @@ export default {
|
|||
}
|
||||
return false;
|
||||
},
|
||||
itemsCount() {
|
||||
return this.list.issuesCount;
|
||||
},
|
||||
countIcon() {
|
||||
return 'issues';
|
||||
},
|
||||
itemsTooltipLabel() {
|
||||
return n__(`%d issue`, `%d issues`, this.itemsCount);
|
||||
return n__(`%d issue`, `%d issues`, this.boardLists?.issuesCount);
|
||||
},
|
||||
chevronTooltip() {
|
||||
return this.list.collapsed ? this.$options.i18n.expand : this.$options.i18n.collapse;
|
||||
|
@ -158,6 +156,23 @@ export default {
|
|||
userCanDrag() {
|
||||
return !this.disabled && isListDraggable(this.list);
|
||||
},
|
||||
isLoading() {
|
||||
return this.$apollo.queries.boardList.loading;
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
boardList: {
|
||||
query: listQuery,
|
||||
variables() {
|
||||
return {
|
||||
id: this.list.id,
|
||||
filters: this.filterParams,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return this.isEpicBoard;
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const localCollapsed = parseBoolean(localStorage.getItem(`${this.uniqueKey}.collapsed`));
|
||||
|
@ -375,10 +390,10 @@ export default {
|
|||
</gl-sprintf>
|
||||
</div>
|
||||
<div v-else>• {{ itemsTooltipLabel }}</div>
|
||||
<div v-if="weightFeatureAvailable">
|
||||
<div v-if="weightFeatureAvailable && !isLoading">
|
||||
•
|
||||
<gl-sprintf :message="__('%{totalWeight} total weight')">
|
||||
<template #totalWeight>{{ list.totalWeight }}</template>
|
||||
<template #totalWeight>{{ boardList.totalWeight }}</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
</gl-tooltip>
|
||||
|
@ -396,14 +411,18 @@ export default {
|
|||
<gl-tooltip :target="() => $refs.itemCount" :title="itemsTooltipLabel" />
|
||||
<span ref="itemCount" class="gl-display-inline-flex gl-align-items-center">
|
||||
<gl-icon class="gl-mr-2" :name="countIcon" />
|
||||
<item-count :items-size="itemsCount" :max-issue-count="list.maxIssueCount" />
|
||||
<item-count
|
||||
v-if="!isLoading"
|
||||
:items-size="isEpicBoard ? list.epicsCount : boardList.issuesCount"
|
||||
:max-issue-count="list.maxIssueCount"
|
||||
/>
|
||||
</span>
|
||||
<!-- EE start -->
|
||||
<template v-if="weightFeatureAvailable && !isEpicBoard">
|
||||
<template v-if="weightFeatureAvailable && !isEpicBoard && !isLoading">
|
||||
<gl-tooltip :target="() => $refs.weightTooltip" :title="weightCountToolTip" />
|
||||
<span ref="weightTooltip" class="gl-display-inline-flex gl-ml-3">
|
||||
<gl-icon class="gl-mr-2" name="weight" />
|
||||
{{ list.totalWeight }}
|
||||
{{ boardList.totalWeight }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- EE end -->
|
||||
|
|
|
@ -1,173 +0,0 @@
|
|||
<script>
|
||||
import { GlLabel } from '@gitlab/ui';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import Api from '~/api';
|
||||
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { isScopedLabel } from '~/lib/utils/common_utils';
|
||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BoardEditableItem,
|
||||
LabelsSelect,
|
||||
GlLabel,
|
||||
},
|
||||
inject: {
|
||||
labelsFetchPath: {
|
||||
default: null,
|
||||
},
|
||||
labelsManagePath: {},
|
||||
labelsFilterBasePath: {},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
oldIid: null,
|
||||
isEditing: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['activeBoardItem', 'projectPathForActiveIssue']),
|
||||
selectedLabels() {
|
||||
const { labels = [] } = this.activeBoardItem;
|
||||
|
||||
return labels.map((label) => ({
|
||||
...label,
|
||||
id: getIdFromGraphQLId(label.id),
|
||||
}));
|
||||
},
|
||||
issueLabels() {
|
||||
const { labels = [] } = this.activeBoardItem;
|
||||
|
||||
return labels.map((label) => ({
|
||||
...label,
|
||||
scoped: isScopedLabel(label),
|
||||
}));
|
||||
},
|
||||
fetchPath() {
|
||||
/*
|
||||
Labels fetched in epic boards are always group-level labels
|
||||
and the correct path are passed from the backend (injected through labelsFetchPath)
|
||||
|
||||
For issue boards, we should always include project-level labels and use a different endpoint.
|
||||
(it requires knowing the project path of a selected issue.)
|
||||
|
||||
Note 1. that we will be using GraphQL to fetch labels when we create a labels select widget.
|
||||
And this component will be removed _wholesale_ https://gitlab.com/gitlab-org/gitlab/-/issues/300653.
|
||||
|
||||
Note 2. Moreover, 'fetchPath' needs to be used as a key for 'labels-select' component to force updates.
|
||||
'labels-select' has its own vuex store and initializes the passed props as states
|
||||
and these states aren't reactively bound to the passed props.
|
||||
*/
|
||||
|
||||
const projectLabelsFetchPath = mergeUrlParams(
|
||||
{ include_ancestor_groups: true },
|
||||
Api.buildUrl(Api.projectLabelsPath).replace(
|
||||
':namespace_path/:project_path',
|
||||
this.projectPathForActiveIssue,
|
||||
),
|
||||
);
|
||||
|
||||
return this.labelsFetchPath || projectLabelsFetchPath;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeBoardItem(_, oldVal) {
|
||||
if (this.isEditing) {
|
||||
this.oldIid = oldVal.iid;
|
||||
} else {
|
||||
this.oldIid = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setActiveBoardItemLabels', 'setError']),
|
||||
async setLabels(payload) {
|
||||
this.loading = true;
|
||||
this.$refs.sidebarItem.collapse();
|
||||
|
||||
try {
|
||||
const addLabelIds = payload.filter((label) => label.set).map((label) => label.id);
|
||||
const removeLabelIds = payload.filter((label) => !label.set).map((label) => label.id);
|
||||
|
||||
const input = {
|
||||
addLabelIds,
|
||||
removeLabelIds,
|
||||
projectPath: this.projectPathForActiveIssue,
|
||||
iid: this.oldIid,
|
||||
};
|
||||
await this.setActiveBoardItemLabels(input);
|
||||
this.oldIid = null;
|
||||
} catch (e) {
|
||||
this.setError({ error: e, message: __('An error occurred while updating labels.') });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async removeLabel(id) {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const removeLabelIds = [getIdFromGraphQLId(id)];
|
||||
const input = { removeLabelIds, projectPath: this.projectPathForActiveIssue };
|
||||
await this.setActiveBoardItemLabels(input);
|
||||
} catch (e) {
|
||||
this.setError({ error: e, message: __('An error occurred when removing the label.') });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<board-editable-item
|
||||
ref="sidebarItem"
|
||||
:title="__('Labels')"
|
||||
:loading="loading"
|
||||
data-testid="sidebar-labels"
|
||||
@open="isEditing = true"
|
||||
@close="isEditing = false"
|
||||
>
|
||||
<template #collapsed>
|
||||
<gl-label
|
||||
v-for="label in issueLabels"
|
||||
:key="label.id"
|
||||
:background-color="label.color"
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
:scoped="label.scoped"
|
||||
:show-close-button="true"
|
||||
:disabled="loading"
|
||||
class="gl-mr-2 gl-mb-2"
|
||||
@close="removeLabel(label.id)"
|
||||
/>
|
||||
</template>
|
||||
<template #default="{ edit }">
|
||||
<labels-select
|
||||
ref="labelsSelect"
|
||||
:key="fetchPath"
|
||||
:allow-label-edit="false"
|
||||
:allow-label-create="false"
|
||||
:allow-multiselect="true"
|
||||
:allow-scoped-labels="true"
|
||||
:selected-labels="selectedLabels"
|
||||
:labels-fetch-path="fetchPath"
|
||||
:labels-manage-path="labelsManagePath"
|
||||
:labels-filter-base-path="labelsFilterBasePath"
|
||||
:labels-list-title="__('Select label')"
|
||||
:dropdown-button-text="__('Choose labels')"
|
||||
:is-editing="edit"
|
||||
variant="sidebar"
|
||||
class="gl-display-block labels gl-w-full"
|
||||
@updateSelectedLabels="setLabels"
|
||||
>
|
||||
{{ __('None') }}
|
||||
</labels-select>
|
||||
</template>
|
||||
</board-editable-item>
|
||||
</template>
|
|
@ -1,75 +0,0 @@
|
|||
<script>
|
||||
import { GlToggle } from '@gitlab/ui';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { __, s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
header: {
|
||||
title: __('Notifications'),
|
||||
/* Any change to subscribeDisabledDescription
|
||||
must be reflected in app/helpers/notifications_helper.rb */
|
||||
subscribeDisabledDescription: __(
|
||||
'Notifications have been disabled by the project or group owner',
|
||||
),
|
||||
},
|
||||
updateSubscribedErrorMessage: s__(
|
||||
'IssueBoards|An error occurred while setting notifications status. Please try again.',
|
||||
),
|
||||
},
|
||||
components: {
|
||||
GlToggle,
|
||||
},
|
||||
inject: ['emailsDisabled'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['activeBoardItem', 'projectPathForActiveIssue', 'isEpicBoard']),
|
||||
isEmailsDisabled() {
|
||||
return this.isEpicBoard ? this.emailsDisabled : this.activeBoardItem.emailsDisabled;
|
||||
},
|
||||
notificationText() {
|
||||
return this.isEmailsDisabled
|
||||
? this.$options.i18n.header.subscribeDisabledDescription
|
||||
: this.$options.i18n.header.title;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setActiveItemSubscribed', 'setError']),
|
||||
async handleToggleSubscription() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await this.setActiveItemSubscribed({
|
||||
subscribed: !this.activeBoardItem.subscribed,
|
||||
projectPath: this.projectPathForActiveIssue,
|
||||
});
|
||||
} catch (error) {
|
||||
this.setError({ error, message: this.$options.i18n.updateSubscribedErrorMessage });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="gl-display-flex gl-align-items-center gl-justify-content-space-between"
|
||||
data-testid="sidebar-notifications"
|
||||
>
|
||||
<span data-testid="notification-header-text"> {{ notificationText }} </span>
|
||||
<gl-toggle
|
||||
v-if="!isEmailsDisabled"
|
||||
:value="activeBoardItem.subscribed"
|
||||
:is-loading="loading"
|
||||
:label="$options.i18n.header.title"
|
||||
label-position="hidden"
|
||||
data-testid="notification-subscribe-toggle"
|
||||
@change="handleToggleSubscription"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -4,7 +4,6 @@ fragment BoardListShared on BoardList {
|
|||
position
|
||||
listType
|
||||
collapsed
|
||||
issuesCount
|
||||
label {
|
||||
id
|
||||
title
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
query BoardList($id: ID!, $filters: BoardIssueInput) {
|
||||
boardList(id: $id, issueFilters: $filters) {
|
||||
id
|
||||
issuesCount
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
|
||||
|
||||
query BoardListEE(
|
||||
query BoardListsEE(
|
||||
$fullPath: ID!
|
||||
$boardId: ID!
|
||||
$id: ID
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
} from 'ee_else_ce/boards/boards_util';
|
||||
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
|
||||
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
|
||||
import totalCountAndWeightQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { queryToObject } from '~/lib/utils/url_utility';
|
||||
|
@ -501,9 +502,10 @@ export default {
|
|||
|
||||
updateIssueOrder: async ({ commit, dispatch, state }, { moveData, mutationVariables = {} }) => {
|
||||
try {
|
||||
const { itemId, fromListId, toListId, moveBeforeId, moveAfterId } = moveData;
|
||||
const { itemId, fromListId, toListId, moveBeforeId, moveAfterId, itemNotInToList } = moveData;
|
||||
const {
|
||||
fullBoardId,
|
||||
filterParams,
|
||||
boardItems: {
|
||||
[itemId]: { iid, referencePath },
|
||||
},
|
||||
|
@ -522,6 +524,67 @@ export default {
|
|||
// 'mutationVariables' allows EE code to pass in extra parameters.
|
||||
...mutationVariables,
|
||||
},
|
||||
update(
|
||||
cache,
|
||||
{
|
||||
data: {
|
||||
issueMoveList: {
|
||||
issue: { weight },
|
||||
},
|
||||
},
|
||||
},
|
||||
) {
|
||||
if (fromListId === toListId) return;
|
||||
|
||||
const updateFromList = () => {
|
||||
const fromList = cache.readQuery({
|
||||
query: totalCountAndWeightQuery,
|
||||
variables: { id: fromListId, filters: filterParams },
|
||||
});
|
||||
|
||||
const updatedFromList = {
|
||||
boardList: {
|
||||
__typename: 'BoardList',
|
||||
id: fromList.boardList.id,
|
||||
issuesCount: fromList.boardList.issuesCount - 1,
|
||||
totalWeight: fromList.boardList.totalWeight - Number(weight),
|
||||
},
|
||||
};
|
||||
|
||||
cache.writeQuery({
|
||||
query: totalCountAndWeightQuery,
|
||||
variables: { id: fromListId, filters: filterParams },
|
||||
data: updatedFromList,
|
||||
});
|
||||
};
|
||||
|
||||
const updateToList = () => {
|
||||
if (!itemNotInToList) return;
|
||||
|
||||
const toList = cache.readQuery({
|
||||
query: totalCountAndWeightQuery,
|
||||
variables: { id: toListId, filters: filterParams },
|
||||
});
|
||||
|
||||
const updatedToList = {
|
||||
boardList: {
|
||||
__typename: 'BoardList',
|
||||
id: toList.boardList.id,
|
||||
issuesCount: toList.boardList.issuesCount + 1,
|
||||
totalWeight: toList.boardList.totalWeight + Number(weight),
|
||||
},
|
||||
};
|
||||
|
||||
cache.writeQuery({
|
||||
query: totalCountAndWeightQuery,
|
||||
variables: { id: toListId, filters: filterParams },
|
||||
data: updatedToList,
|
||||
});
|
||||
};
|
||||
|
||||
updateFromList();
|
||||
updateToList();
|
||||
},
|
||||
});
|
||||
|
||||
if (data?.issueMoveList?.errors.length || !data.issueMoveList) {
|
||||
|
@ -565,7 +628,7 @@ export default {
|
|||
},
|
||||
|
||||
addListNewIssue: (
|
||||
{ state: { boardConfig, boardType, fullPath }, dispatch, commit },
|
||||
{ state: { boardConfig, boardType, fullPath, filterParams }, dispatch, commit },
|
||||
{ issueInput, list, placeholderId = `tmp-${new Date().getTime()}` },
|
||||
) => {
|
||||
const input = formatIssueInput(issueInput, boardConfig);
|
||||
|
@ -581,6 +644,27 @@ export default {
|
|||
.mutate({
|
||||
mutation: issueCreateMutation,
|
||||
variables: { input },
|
||||
update(cache) {
|
||||
const fromList = cache.readQuery({
|
||||
query: totalCountAndWeightQuery,
|
||||
variables: { id: list.id, filters: filterParams },
|
||||
});
|
||||
|
||||
const updatedList = {
|
||||
boardList: {
|
||||
__typename: 'BoardList',
|
||||
id: fromList.boardList.id,
|
||||
issuesCount: fromList.boardList.issuesCount + 1,
|
||||
totalWeight: fromList.boardList.totalWeight,
|
||||
},
|
||||
};
|
||||
|
||||
cache.writeQuery({
|
||||
query: totalCountAndWeightQuery,
|
||||
variables: { id: list.id, filters: filterParams },
|
||||
data: updatedList,
|
||||
});
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.createIssue.errors.length) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
|
||||
import { sortMilestonesByDueDate } from '~/milestones/utils';
|
||||
import { mergeUrlParams } from '../lib/utils/url_utility';
|
||||
import DropdownAjaxFilter from './dropdown_ajax_filter';
|
||||
import DropdownEmoji from './dropdown_emoji';
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import $ from 'jquery';
|
||||
import initDatePicker from '~/behaviors/date_picker';
|
||||
import GLForm from '~/gl_form';
|
||||
import ZenMode from '~/zen_mode';
|
||||
|
||||
export default (initGFM = true) => {
|
||||
new ZenMode(); // eslint-disable-line no-new
|
||||
initDatePicker();
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new GLForm($('.milestone-form'), {
|
||||
emojis: true,
|
||||
members: initGFM,
|
||||
issues: initGFM,
|
||||
mergeRequests: initGFM,
|
||||
epics: initGFM,
|
||||
milestones: initGFM,
|
||||
labels: initGFM,
|
||||
snippets: initGFM,
|
||||
vulnerabilities: initGFM,
|
||||
});
|
||||
};
|
|
@ -1,10 +1,58 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import initDatePicker from '~/behaviors/date_picker';
|
||||
import GLForm from '~/gl_form';
|
||||
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
|
||||
import Milestone from '~/milestones/milestone';
|
||||
import Sidebar from '~/right_sidebar';
|
||||
import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import ZenMode from '~/zen_mode';
|
||||
import DeleteMilestoneModal from './components/delete_milestone_modal.vue';
|
||||
import PromoteMilestoneModal from './components/promote_milestone_modal.vue';
|
||||
import eventHub from './event_hub';
|
||||
|
||||
export default () => {
|
||||
export function initForm(initGFM = true) {
|
||||
new ZenMode(); // eslint-disable-line no-new
|
||||
initDatePicker();
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new GLForm($('.milestone-form'), {
|
||||
emojis: true,
|
||||
members: initGFM,
|
||||
issues: initGFM,
|
||||
mergeRequests: initGFM,
|
||||
epics: initGFM,
|
||||
milestones: initGFM,
|
||||
labels: initGFM,
|
||||
snippets: initGFM,
|
||||
vulnerabilities: initGFM,
|
||||
});
|
||||
}
|
||||
|
||||
export function initShow() {
|
||||
new Milestone(); // eslint-disable-line no-new
|
||||
new Sidebar(); // eslint-disable-line no-new
|
||||
new MountMilestoneSidebar(); // eslint-disable-line no-new
|
||||
}
|
||||
|
||||
export function initPromoteMilestoneModal() {
|
||||
Vue.use(Translate);
|
||||
|
||||
const promoteMilestoneModal = document.getElementById('promote-milestone-modal');
|
||||
if (!promoteMilestoneModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Vue({
|
||||
el: promoteMilestoneModal,
|
||||
render(createElement) {
|
||||
return createElement(PromoteMilestoneModal);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function initDeleteMilestoneModal() {
|
||||
Vue.use(Translate);
|
||||
|
||||
const onRequestFinished = ({ milestoneUrl, successful }) => {
|
||||
|
@ -72,4 +120,4 @@ export default () => {
|
|||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
/* eslint-disable no-new */
|
||||
|
||||
import Milestone from '~/milestones/milestone';
|
||||
import Sidebar from '~/right_sidebar';
|
||||
import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar';
|
||||
|
||||
export default () => {
|
||||
new Milestone();
|
||||
new Sidebar();
|
||||
new MountMilestoneSidebar();
|
||||
};
|
|
@ -6,7 +6,7 @@ import { template, escape } from 'lodash';
|
|||
import Api from '~/api';
|
||||
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
|
||||
import { sortMilestonesByDueDate } from '~/milestones/utils';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility';
|
||||
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import PromoteMilestoneModal from './components/promote_milestone_modal.vue';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
export default () => {
|
||||
const promoteMilestoneModal = document.getElementById('promote-milestone-modal');
|
||||
if (!promoteMilestoneModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Vue({
|
||||
el: promoteMilestoneModal,
|
||||
render(createElement) {
|
||||
return createElement(PromoteMilestoneModal);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,3 +1,3 @@
|
|||
import initForm from '~/milestones/form';
|
||||
import { initForm } from '~/milestones';
|
||||
|
||||
initForm();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initForm from '~/milestones/form';
|
||||
import { initForm } from '~/milestones';
|
||||
|
||||
initForm();
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import initDeleteMilestoneModal from '~/milestones/delete_milestone_modal_init';
|
||||
import initMilestonesShow from '~/milestones/init_milestones_show';
|
||||
import { initDeleteMilestoneModal, initShow } from '~/milestones';
|
||||
|
||||
initMilestonesShow();
|
||||
initShow();
|
||||
initDeleteMilestoneModal();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initForm from '~/milestones/form';
|
||||
import { initForm } from '~/milestones';
|
||||
|
||||
initForm();
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import initDeleteMilestoneModal from '~/milestones/delete_milestone_modal_init';
|
||||
import initPromoteMilestoneModal from '~/milestones/promote_milestone_modal_init';
|
||||
import { initDeleteMilestoneModal, initPromoteMilestoneModal } from '~/milestones';
|
||||
|
||||
initDeleteMilestoneModal();
|
||||
initPromoteMilestoneModal();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initForm from '~/milestones/form';
|
||||
import { initForm } from '~/milestones';
|
||||
|
||||
initForm();
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import initMilestonesShow from '~/milestones/init_milestones_show';
|
||||
import initDeleteMilestoneModal from '~/milestones/delete_milestone_modal_init';
|
||||
import initPromoteMilestoneModal from '~/milestones/promote_milestone_modal_init';
|
||||
import { initDeleteMilestoneModal, initPromoteMilestoneModal, initShow } from '~/milestones';
|
||||
|
||||
initMilestonesShow();
|
||||
initShow();
|
||||
initDeleteMilestoneModal();
|
||||
initPromoteMilestoneModal();
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
<script>
|
||||
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
|
||||
import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
|
||||
import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LabelsSelectWidget,
|
||||
},
|
||||
variant: DropdownVariant.Sidebar,
|
||||
inject: ['allowLabelEdit', 'iid', 'fullPath', 'issuableType', 'projectIssuesPath'],
|
||||
data() {
|
||||
return {
|
||||
LabelType,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<labels-select-widget
|
||||
class="block labels js-labels-block"
|
||||
:iid="iid"
|
||||
:full-path="fullPath"
|
||||
:allow-label-remove="allowLabelEdit"
|
||||
:allow-multiselect="true"
|
||||
:footer-create-label-title="__('Create project label')"
|
||||
:footer-manage-label-title="__('Manage project labels')"
|
||||
:labels-create-title="__('Create project label')"
|
||||
:labels-filter-base-path="projectIssuesPath"
|
||||
:variant="$options.variant"
|
||||
:issuable-type="issuableType"
|
||||
workspace-type="project"
|
||||
:attr-workspace-path="fullPath"
|
||||
:label-create-type="LabelType.project"
|
||||
data-qa-selector="labels_block"
|
||||
>
|
||||
{{ __('None') }}
|
||||
</labels-select-widget>
|
||||
</template>
|
|
@ -12,6 +12,7 @@ import {
|
|||
isInIncidentPage,
|
||||
parseBoolean,
|
||||
} from '~/lib/utils/common_utils';
|
||||
import { __ } from '~/locale';
|
||||
import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
|
||||
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
|
||||
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
|
||||
|
@ -23,10 +24,11 @@ import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_wid
|
|||
import { apolloProvider } from '~/sidebar/graphql';
|
||||
import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
|
||||
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
|
||||
import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
|
||||
import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
|
||||
import Translate from '../vue_shared/translate';
|
||||
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
|
||||
import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
|
||||
import SidebarLabels from './components/labels/sidebar_labels.vue';
|
||||
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
|
||||
import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
|
||||
import SidebarSeverity from './components/severity/sidebar_severity.vue';
|
||||
|
@ -264,7 +266,6 @@ function mountMilestoneSelect() {
|
|||
|
||||
export function mountSidebarLabels() {
|
||||
const el = document.querySelector('.js-sidebar-labels');
|
||||
const { fullPath } = getSidebarOptions();
|
||||
|
||||
if (!el) {
|
||||
return false;
|
||||
|
@ -273,22 +274,43 @@ export function mountSidebarLabels() {
|
|||
return new Vue({
|
||||
el,
|
||||
apolloProvider,
|
||||
|
||||
components: {
|
||||
LabelsSelectWidget,
|
||||
},
|
||||
provide: {
|
||||
...el.dataset,
|
||||
fullPath,
|
||||
canUpdate: parseBoolean(el.dataset.canEdit),
|
||||
allowLabelCreate: parseBoolean(el.dataset.allowLabelCreate),
|
||||
allowLabelEdit: parseBoolean(el.dataset.canEdit),
|
||||
allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels),
|
||||
initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels),
|
||||
variant: DropdownVariant.Sidebar,
|
||||
canUpdate: parseBoolean(el.dataset.canEdit),
|
||||
isClassicSidebar: true,
|
||||
issuableType:
|
||||
isInIssuePage() || isInIncidentPage() || isInDesignPage()
|
||||
? IssuableType.Issue
|
||||
: IssuableType.MergeRequest,
|
||||
},
|
||||
render: (createElement) => createElement(SidebarLabels),
|
||||
render: (createElement) =>
|
||||
createElement('labels-select-widget', {
|
||||
props: {
|
||||
iid: String(el.dataset.iid),
|
||||
fullPath: el.dataset.projectPath,
|
||||
allowLabelRemove: parseBoolean(el.dataset.canEdit),
|
||||
allowMultiselect: true,
|
||||
footerCreateLabelTitle: __('Create project label'),
|
||||
footerManageLabelTitle: __('Manage project labels'),
|
||||
labelsCreateTitle: __('Create project label'),
|
||||
labelsFilterBasePath: el.dataset.projectIssuesPath,
|
||||
variant: DropdownVariant.Sidebar,
|
||||
issuableType:
|
||||
isInIssuePage() || isInIncidentPage() || isInDesignPage()
|
||||
? IssuableType.Issue
|
||||
: IssuableType.MergeRequest,
|
||||
workspaceType: 'project',
|
||||
attrWorkspacePath: el.dataset.projectPath,
|
||||
labelCreateType: LabelType.project,
|
||||
},
|
||||
class: ['block labels js-labels-block'],
|
||||
scopedSlots: {
|
||||
default: () => __('None'),
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
|
||||
import createFlash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
|
||||
import { sortMilestonesByDueDate } from '~/milestones/utils';
|
||||
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
|
||||
import { DEFAULT_MILESTONES } from '../constants';
|
||||
import { stripQuotes } from '../filtered_search_utils';
|
||||
|
|
|
@ -291,6 +291,7 @@ export default {
|
|||
'is-standalone': isDropdownVariantStandalone(variant),
|
||||
'is-embedded': isDropdownVariantEmbedded(variant),
|
||||
}"
|
||||
data-qa-selector="labels_block"
|
||||
>
|
||||
<template v-if="isDropdownVariantSidebar(variant)">
|
||||
<dropdown-value-collapsed
|
||||
|
|
|
@ -67,7 +67,6 @@ module NotificationsHelper
|
|||
when :custom
|
||||
_('You will only receive notifications for the events you choose')
|
||||
when :owner_disabled
|
||||
# Any change must be reflected in board_sidebar_subscription.vue
|
||||
_('Notifications have been disabled by the project or group owner')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -958,7 +958,7 @@ module Ci
|
|||
.limit(100)
|
||||
.pluck(:expanded_environment_name)
|
||||
|
||||
Environment.where(project: project, name: expanded_environment_names).with_deployments
|
||||
Environment.where(project: project, name: expanded_environment_names).with_deployment(sha)
|
||||
else
|
||||
environment_ids = self_and_descendants.joins(:deployments).select(:'deployments.environment_id')
|
||||
|
||||
|
|
|
@ -89,7 +89,6 @@ class Environment < ApplicationRecord
|
|||
|
||||
scope :for_project, -> (project) { where(project_id: project) }
|
||||
scope :for_tier, -> (tier) { where(tier: tier).where.not(tier: nil) }
|
||||
scope :with_deployments, -> { where('EXISTS (?)', Deployment.select(1).where('deployments.environment_id = environments.id')) }
|
||||
scope :with_deployment, -> (sha) { where('EXISTS (?)', Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha)) }
|
||||
scope :unfoldered, -> { where(environment_type: nil) }
|
||||
scope :with_rank, -> do
|
||||
|
|
|
@ -7,5 +7,9 @@ module Namespaces
|
|||
def self.sti_name
|
||||
'Project'
|
||||
end
|
||||
|
||||
def self.polymorphic_name
|
||||
'Namespaces::ProjectNamespace'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: load_balancing_for_update_all_mirrors_worker
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64526
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334162
|
||||
milestone: '14.1'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: true
|
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
|
@ -36,6 +36,10 @@ graph TD
|
|||
With the addition of [multi-level epics](../epics/manage_epics.md#multi-level-child-epics) and up to
|
||||
seven levels of nested epics, you can achieve the following hierarchy:
|
||||
|
||||
<!--
|
||||
Image below was generated with the following Mermaid code.
|
||||
Attached as an image because a rendered diagram doesn't look clear on the docs page.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
direction TD
|
||||
|
@ -46,6 +50,10 @@ classDiagram
|
|||
Epic "1" *-- "0..*" Issue
|
||||
```
|
||||
|
||||
-->
|
||||
|
||||
![Diagram showing possible relationships of multi-level epics](img/hierarchy_with_multi_level_epics.png)
|
||||
|
||||
## View ancestry of an epic
|
||||
|
||||
In an epic, you can view the ancestors as parents in the right sidebar under **Ancestors**.
|
||||
|
|
|
@ -3650,9 +3650,6 @@ msgstr ""
|
|||
msgid "An error occurred previewing the blob"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred when removing the label."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred when updating the title"
|
||||
msgstr ""
|
||||
|
||||
|
@ -7050,9 +7047,6 @@ msgstr ""
|
|||
msgid "Choose file…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Choose labels"
|
||||
msgstr ""
|
||||
|
||||
msgid "Choose specific groups or storage shards"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19539,9 +19533,6 @@ msgstr ""
|
|||
msgid "IssueAnalytics|Weight"
|
||||
msgstr ""
|
||||
|
||||
msgid "IssueBoards|An error occurred while setting notifications status. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "IssueBoards|Board"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -9,21 +9,19 @@ module Gitlab
|
|||
link :storage_tab, id: 'storage-quota'
|
||||
link :buy_ci_minutes, text: 'Buy additional minutes'
|
||||
link :buy_storage, text: /Purchase more storage/
|
||||
strong :additional_minutes, text: 'Additional minutes'
|
||||
strong :plan_minutes, text: 'Current period usage'
|
||||
div :purchased_usage, 'data-testid': 'purchased-usage'
|
||||
div(:plan_minutes_usage) { plan_minutes_element.following_sibling.span }
|
||||
div(:additional_minutes_usage) { additional_minutes_element.following_sibling.span }
|
||||
div :plan_ci_minutes
|
||||
div :additional_ci_minutes
|
||||
span :purchased_usage_total
|
||||
div :ci_purchase_successful_alert, text: /You have successfully purchased CI minutes/
|
||||
div :storage_purchase_successful_alert, text: /You have successfully purchased a storage/
|
||||
h4 :storage_available_alert, text: /purchased storage is available/
|
||||
|
||||
def plan_minutes_limits
|
||||
plan_minutes_usage[%r{([^/ ]+)$}]
|
||||
def plan_ci_limits
|
||||
plan_ci_minutes_element.span.text[%r{([^/ ]+)$}]
|
||||
end
|
||||
|
||||
def additional_limits
|
||||
additional_minutes_usage[%r{([^/ ]+)$}]
|
||||
def additional_ci_limits
|
||||
additional_ci_minutes_element.span.text[%r{([^/ ]+)$}]
|
||||
end
|
||||
|
||||
# Waits and Checks if storage available alert presents on the page
|
||||
|
@ -40,7 +38,7 @@ module Gitlab
|
|||
# @return [Float] Total purchased storage value in GiB
|
||||
def total_purchased_storage
|
||||
storage_available_alert_element.wait_until(&:present?)
|
||||
purchased_usage_element.p.spans[3].text.to_f
|
||||
purchased_usage_total.to_f
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,7 @@ module QA
|
|||
element :more_assignees_link
|
||||
end
|
||||
|
||||
base.view 'app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue' do
|
||||
base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue' do
|
||||
element :labels_block
|
||||
end
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
import BoardCard from '~/boards/components/board_card.vue';
|
||||
|
@ -6,7 +7,15 @@ import BoardList from '~/boards/components/board_list.vue';
|
|||
import BoardNewIssue from '~/boards/components/board_new_issue.vue';
|
||||
import BoardNewItem from '~/boards/components/board_new_item.vue';
|
||||
import defaultState from '~/boards/stores/state';
|
||||
import { mockList, mockIssuesByListId, issues, mockGroupProjects } from './mock_data';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import listQuery from '~/boards/graphql/board_lists_deferred.query.graphql';
|
||||
import {
|
||||
mockList,
|
||||
mockIssuesByListId,
|
||||
issues,
|
||||
mockGroupProjects,
|
||||
boardListQueryResponse,
|
||||
} from './mock_data';
|
||||
|
||||
export default function createComponent({
|
||||
listIssueProps = {},
|
||||
|
@ -15,16 +24,23 @@ export default function createComponent({
|
|||
actions = {},
|
||||
getters = {},
|
||||
provide = {},
|
||||
data = {},
|
||||
state = defaultState,
|
||||
stubs = {
|
||||
BoardNewIssue,
|
||||
BoardNewItem,
|
||||
BoardCard,
|
||||
},
|
||||
issuesCount,
|
||||
} = {}) {
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueApollo);
|
||||
localVue.use(Vuex);
|
||||
|
||||
const fakeApollo = createMockApollo([
|
||||
[listQuery, jest.fn().mockResolvedValue(boardListQueryResponse(issuesCount))],
|
||||
]);
|
||||
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
selectedProject: mockGroupProjects[0],
|
||||
|
@ -68,6 +84,7 @@ export default function createComponent({
|
|||
}
|
||||
|
||||
const component = shallowMount(BoardList, {
|
||||
apolloProvider: fakeApollo,
|
||||
localVue,
|
||||
store,
|
||||
propsData: {
|
||||
|
@ -87,6 +104,11 @@ export default function createComponent({
|
|||
...provide,
|
||||
},
|
||||
stubs,
|
||||
data() {
|
||||
return {
|
||||
...data,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return component;
|
||||
|
|
|
@ -38,7 +38,7 @@ describe('Board list component', () => {
|
|||
|
||||
describe('When Expanded', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
wrapper = createComponent({ issuesCount: 1 });
|
||||
});
|
||||
|
||||
it('renders component', () => {
|
||||
|
@ -97,14 +97,6 @@ describe('Board list component', () => {
|
|||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('.board-list-count').attributes('data-issue-id')).toBe('-1');
|
||||
});
|
||||
|
||||
it('shows how many more issues to load', async () => {
|
||||
wrapper.vm.showCount = true;
|
||||
wrapper.setProps({ list: { issuesCount: 20 } });
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues');
|
||||
});
|
||||
});
|
||||
|
||||
describe('load more issues', () => {
|
||||
|
@ -113,9 +105,7 @@ describe('Board list component', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({
|
||||
listProps: { issuesCount: 25 },
|
||||
});
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
it('does not load issues if already loading', () => {
|
||||
|
@ -131,13 +121,27 @@ describe('Board list component', () => {
|
|||
it('shows loading more spinner', async () => {
|
||||
wrapper = createComponent({
|
||||
state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } },
|
||||
data: {
|
||||
showCount: true,
|
||||
},
|
||||
});
|
||||
wrapper.vm.showCount = true;
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findIssueCountLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows how many more issues to load', async () => {
|
||||
// wrapper.vm.showCount = true;
|
||||
wrapper = createComponent({
|
||||
data: {
|
||||
showCount: true,
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues');
|
||||
});
|
||||
});
|
||||
|
||||
describe('max issue count warning', () => {
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import Vuex from 'vuex';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
import { mockLabelList } from 'jest/boards/mock_data';
|
||||
import { boardListQueryResponse, mockLabelList } from 'jest/boards/mock_data';
|
||||
import BoardListHeader from '~/boards/components/board_list_header.vue';
|
||||
import { ListType } from '~/boards/constants';
|
||||
import listQuery from '~/boards/graphql/board_lists_deferred.query.graphql';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
||||
localVue.use(Vuex);
|
||||
Vue.use(VueApollo);
|
||||
Vue.use(Vuex);
|
||||
|
||||
describe('Board List Header Component', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
let fakeApollo;
|
||||
|
||||
const updateListSpy = jest.fn();
|
||||
const toggleListCollapsedSpy = jest.fn();
|
||||
|
@ -20,6 +24,7 @@ describe('Board List Header Component', () => {
|
|||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
fakeApollo = null;
|
||||
|
||||
localStorage.clear();
|
||||
});
|
||||
|
@ -29,6 +34,7 @@ describe('Board List Header Component', () => {
|
|||
collapsed = false,
|
||||
withLocalStorage = true,
|
||||
currentUserId = 1,
|
||||
listQueryHandler = jest.fn().mockResolvedValue(boardListQueryResponse()),
|
||||
} = {}) => {
|
||||
const boardId = '1';
|
||||
|
||||
|
@ -56,10 +62,12 @@ describe('Board List Header Component', () => {
|
|||
getters: { isEpicBoard: () => false },
|
||||
});
|
||||
|
||||
fakeApollo = createMockApollo([[listQuery, listQueryHandler]]);
|
||||
|
||||
wrapper = extendedWrapper(
|
||||
shallowMount(BoardListHeader, {
|
||||
apolloProvider: fakeApollo,
|
||||
store,
|
||||
localVue,
|
||||
propsData: {
|
||||
disabled: false,
|
||||
list: listMock,
|
||||
|
|
|
@ -1,168 +0,0 @@
|
|||
import { GlLabel } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import {
|
||||
labels as TEST_LABELS,
|
||||
mockIssue as TEST_ISSUE,
|
||||
mockIssueFullPath as TEST_ISSUE_FULLPATH,
|
||||
} from 'jest/boards/mock_data';
|
||||
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
|
||||
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
|
||||
import { createStore } from '~/boards/stores';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
|
||||
const TEST_LABELS_PAYLOAD = TEST_LABELS.map((label) => ({ ...label, set: true }));
|
||||
const TEST_LABELS_TITLES = TEST_LABELS.map((label) => label.title);
|
||||
|
||||
describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
store = null;
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
const createWrapper = ({ labels = [], providedValues = {} } = {}) => {
|
||||
store = createStore();
|
||||
store.state.boardItems = { [TEST_ISSUE.id]: { ...TEST_ISSUE, labels } };
|
||||
store.state.activeId = TEST_ISSUE.id;
|
||||
|
||||
wrapper = shallowMount(BoardSidebarLabelsSelect, {
|
||||
store,
|
||||
provide: {
|
||||
canUpdate: true,
|
||||
labelsManagePath: TEST_HOST,
|
||||
labelsFilterBasePath: TEST_HOST,
|
||||
...providedValues,
|
||||
},
|
||||
stubs: {
|
||||
BoardEditableItem,
|
||||
LabelsSelect: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findLabelsSelect = () => wrapper.find({ ref: 'labelsSelect' });
|
||||
const findLabelsTitles = () =>
|
||||
wrapper.findAll(GlLabel).wrappers.map((item) => item.props('title'));
|
||||
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
|
||||
|
||||
describe('when labelsFetchPath is provided', () => {
|
||||
it('uses injected labels fetch path', () => {
|
||||
createWrapper({ providedValues: { labelsFetchPath: 'foobar' } });
|
||||
|
||||
expect(findLabelsSelect().props('labelsFetchPath')).toEqual('foobar');
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the default project label endpoint', () => {
|
||||
createWrapper();
|
||||
|
||||
expect(findLabelsSelect().props('labelsFetchPath')).toEqual(
|
||||
`/${TEST_ISSUE_FULLPATH}/-/labels?include_ancestor_groups=true`,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders "None" when no labels are selected', () => {
|
||||
createWrapper();
|
||||
|
||||
expect(findCollapsed().text()).toBe('None');
|
||||
});
|
||||
|
||||
it('renders labels when set', () => {
|
||||
createWrapper({ labels: TEST_LABELS });
|
||||
|
||||
expect(findLabelsTitles()).toEqual(TEST_LABELS_TITLES);
|
||||
});
|
||||
|
||||
describe('when labels are submitted', () => {
|
||||
beforeEach(async () => {
|
||||
createWrapper();
|
||||
|
||||
jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => TEST_LABELS);
|
||||
findLabelsSelect().vm.$emit('updateSelectedLabels', TEST_LABELS_PAYLOAD);
|
||||
store.state.boardItems[TEST_ISSUE.id].labels = TEST_LABELS;
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('collapses sidebar and renders labels', () => {
|
||||
expect(findCollapsed().isVisible()).toBe(true);
|
||||
expect(findLabelsTitles()).toEqual(TEST_LABELS_TITLES);
|
||||
});
|
||||
|
||||
it('commits change to the server', () => {
|
||||
expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({
|
||||
addLabelIds: TEST_LABELS.map((label) => label.id),
|
||||
projectPath: TEST_ISSUE_FULLPATH,
|
||||
removeLabelIds: [],
|
||||
iid: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when labels are updated over existing labels', () => {
|
||||
const testLabelsPayload = [
|
||||
{ id: 5, set: true },
|
||||
{ id: 6, set: false },
|
||||
{ id: 7, set: true },
|
||||
];
|
||||
const expectedLabels = [{ id: 5 }, { id: 7 }];
|
||||
|
||||
beforeEach(async () => {
|
||||
createWrapper({ labels: TEST_LABELS });
|
||||
|
||||
jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => expectedLabels);
|
||||
findLabelsSelect().vm.$emit('updateSelectedLabels', testLabelsPayload);
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('commits change to the server', () => {
|
||||
expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({
|
||||
addLabelIds: [5, 7],
|
||||
removeLabelIds: [6],
|
||||
projectPath: TEST_ISSUE_FULLPATH,
|
||||
iid: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when removing individual labels', () => {
|
||||
const testLabel = TEST_LABELS[0];
|
||||
|
||||
beforeEach(async () => {
|
||||
createWrapper({ labels: [testLabel] });
|
||||
|
||||
jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it('commits change to the server', () => {
|
||||
wrapper.find(GlLabel).vm.$emit('close', testLabel);
|
||||
|
||||
expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({
|
||||
removeLabelIds: [getIdFromGraphQLId(testLabel.id)],
|
||||
projectPath: TEST_ISSUE_FULLPATH,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the mutation fails', () => {
|
||||
beforeEach(async () => {
|
||||
createWrapper({ labels: TEST_LABELS });
|
||||
|
||||
jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => {
|
||||
throw new Error(['failed mutation']);
|
||||
});
|
||||
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
|
||||
findLabelsSelect().vm.$emit('updateSelectedLabels', [{ id: '?' }]);
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('collapses sidebar and renders former issue weight', () => {
|
||||
expect(findCollapsed().isVisible()).toBe(true);
|
||||
expect(findLabelsTitles()).toEqual(TEST_LABELS_TITLES);
|
||||
expect(wrapper.vm.setError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,163 +0,0 @@
|
|||
import { GlToggle, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
|
||||
import { createStore } from '~/boards/stores';
|
||||
import * as types from '~/boards/stores/mutation_types';
|
||||
import { mockActiveIssue } from '../../mock_data';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
|
||||
const findNotificationHeader = () => wrapper.find("[data-testid='notification-header-text']");
|
||||
const findToggle = () => wrapper.findComponent(GlToggle);
|
||||
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
|
||||
const createComponent = (activeBoardItem = { ...mockActiveIssue }) => {
|
||||
store = createStore();
|
||||
store.state.boardItems = { [activeBoardItem.id]: activeBoardItem };
|
||||
store.state.activeId = activeBoardItem.id;
|
||||
|
||||
wrapper = mount(BoardSidebarSubscription, {
|
||||
store,
|
||||
provide: {
|
||||
emailsDisabled: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
store = null;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Board sidebar subscription component template', () => {
|
||||
it('displays "notifications" heading', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findNotificationHeader().text()).toBe('Notifications');
|
||||
});
|
||||
|
||||
it('renders toggle with label', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findToggle().props('label')).toBe(BoardSidebarSubscription.i18n.header.title);
|
||||
});
|
||||
|
||||
it('renders toggle as "off" when currently not subscribed', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findToggle().exists()).toBe(true);
|
||||
expect(findToggle().props('value')).toBe(false);
|
||||
});
|
||||
|
||||
it('renders toggle as "on" when currently subscribed', () => {
|
||||
createComponent({
|
||||
...mockActiveIssue,
|
||||
subscribed: true,
|
||||
});
|
||||
|
||||
expect(findToggle().exists()).toBe(true);
|
||||
expect(findToggle().props('value')).toBe(true);
|
||||
});
|
||||
|
||||
describe('when notification emails have been disabled', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
...mockActiveIssue,
|
||||
emailsDisabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('displays a message that notification have been disabled', () => {
|
||||
expect(findNotificationHeader().text()).toBe(
|
||||
'Notifications have been disabled by the project or group owner',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render the toggle button', () => {
|
||||
expect(findToggle().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Board sidebar subscription component `behavior`', () => {
|
||||
const mockSetActiveIssueSubscribed = (subscribedState) => {
|
||||
jest.spyOn(wrapper.vm, 'setActiveItemSubscribed').mockImplementation(async () => {
|
||||
store.commit(types.UPDATE_BOARD_ITEM_BY_ID, {
|
||||
itemId: mockActiveIssue.id,
|
||||
prop: 'subscribed',
|
||||
value: subscribedState,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
it('subscribing to notification', async () => {
|
||||
createComponent();
|
||||
mockSetActiveIssueSubscribed(true);
|
||||
|
||||
expect(findGlLoadingIcon().exists()).toBe(false);
|
||||
|
||||
findToggle().vm.$emit('change');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findGlLoadingIcon().exists()).toBe(true);
|
||||
expect(wrapper.vm.setActiveItemSubscribed).toHaveBeenCalledWith({
|
||||
subscribed: true,
|
||||
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findGlLoadingIcon().exists()).toBe(false);
|
||||
expect(findToggle().props('value')).toBe(true);
|
||||
});
|
||||
|
||||
it('unsubscribing from notification', async () => {
|
||||
createComponent({
|
||||
...mockActiveIssue,
|
||||
subscribed: true,
|
||||
});
|
||||
mockSetActiveIssueSubscribed(false);
|
||||
|
||||
expect(findGlLoadingIcon().exists()).toBe(false);
|
||||
|
||||
findToggle().vm.$emit('change');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.setActiveItemSubscribed).toHaveBeenCalledWith({
|
||||
subscribed: false,
|
||||
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
|
||||
});
|
||||
expect(findGlLoadingIcon().exists()).toBe(true);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findGlLoadingIcon().exists()).toBe(false);
|
||||
expect(findToggle().props('value')).toBe(false);
|
||||
});
|
||||
|
||||
it('flashes an error message when setting the subscribed state fails', async () => {
|
||||
createComponent();
|
||||
jest.spyOn(wrapper.vm, 'setActiveItemSubscribed').mockImplementation(async () => {
|
||||
throw new Error();
|
||||
});
|
||||
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
|
||||
|
||||
findToggle().vm.$emit('change');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.vm.setError).toHaveBeenCalled();
|
||||
expect(wrapper.vm.setError.mock.calls[0][0].message).toBe(
|
||||
wrapper.vm.$options.i18n.updateSubscribedErrorMessage,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -662,3 +662,14 @@ export const mockGroupLabelsResponse = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const boardListQueryResponse = (issuesCount = 20) => ({
|
||||
data: {
|
||||
boardList: {
|
||||
__typename: 'BoardList',
|
||||
id: 'gid://gitlab/BoardList/5',
|
||||
totalWeight: 5,
|
||||
issuesCount,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1241,6 +1241,7 @@ describe('updateIssueOrder', () => {
|
|||
moveBeforeId: undefined,
|
||||
moveAfterId: undefined,
|
||||
},
|
||||
update: expect.anything(),
|
||||
};
|
||||
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
||||
data: {
|
||||
|
@ -1447,6 +1448,7 @@ describe('addListNewIssue', () => {
|
|||
variables: {
|
||||
input: formatIssueInput(mockIssue, stateWithBoardConfig.boardConfig),
|
||||
},
|
||||
update: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1478,6 +1480,7 @@ describe('addListNewIssue', () => {
|
|||
variables: {
|
||||
input: formatIssueInput(issue, stateWithBoardConfig.boardConfig),
|
||||
},
|
||||
update: expect.anything(),
|
||||
});
|
||||
expect(payload.labelIds).toEqual(['gid://gitlab/GroupLabel/4', 'gid://gitlab/GroupLabel/5']);
|
||||
expect(payload.assigneeIds).toEqual(['gid://gitlab/User/1', 'gid://gitlab/User/2']);
|
||||
|
|
|
@ -7,7 +7,7 @@ import Vuex from 'vuex';
|
|||
import { ENTER_KEY } from '~/lib/utils/keys';
|
||||
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
|
||||
import createStore from '~/milestones/stores/';
|
||||
import { projectMilestones, groupMilestones } from './mock_data';
|
||||
import { projectMilestones, groupMilestones } from '../mock_data';
|
||||
|
||||
const extraLinks = [
|
||||
{ text: 'Create new', url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/new' },
|
|
@ -1,5 +1,5 @@
|
|||
import { useFakeDate } from 'helpers/fake_date';
|
||||
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
|
||||
import { sortMilestonesByDueDate } from '~/milestones/utils';
|
||||
|
||||
describe('sortMilestonesByDueDate', () => {
|
||||
useFakeDate(2021, 6, 22);
|
|
@ -1,65 +0,0 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import SidebarLabels from '~/sidebar/components/labels/sidebar_labels.vue';
|
||||
import {
|
||||
DropdownVariant,
|
||||
LabelType,
|
||||
} from '~/vue_shared/components/sidebar/labels_select_widget/constants';
|
||||
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
|
||||
|
||||
describe('sidebar labels', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultProps = {
|
||||
allowLabelEdit: true,
|
||||
iid: '1',
|
||||
issuableType: 'issue',
|
||||
projectIssuesPath: '/gitlab-org/gitlab-test/-/issues',
|
||||
fullPath: 'gitlab-org/gitlab-test',
|
||||
};
|
||||
|
||||
const findLabelsSelect = () => wrapper.find(LabelsSelect);
|
||||
|
||||
const mountComponent = (props = {}) => {
|
||||
wrapper = shallowMount(SidebarLabels, {
|
||||
provide: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('LabelsSelect props', () => {
|
||||
describe.each`
|
||||
issuableType
|
||||
${'issue'}
|
||||
${'merge_request'}
|
||||
`('issuableType $issuableType', ({ issuableType }) => {
|
||||
beforeEach(() => {
|
||||
mountComponent({ issuableType });
|
||||
});
|
||||
|
||||
it('has expected props', () => {
|
||||
expect(findLabelsSelect().props()).toMatchObject({
|
||||
iid: defaultProps.iid,
|
||||
fullPath: defaultProps.fullPath,
|
||||
allowLabelRemove: defaultProps.allowLabelEdit,
|
||||
allowMultiselect: true,
|
||||
footerCreateLabelTitle: 'Create project label',
|
||||
footerManageLabelTitle: 'Manage project labels',
|
||||
labelsCreateTitle: 'Create project label',
|
||||
labelsFilterBasePath: defaultProps.projectIssuesPath,
|
||||
variant: DropdownVariant.Sidebar,
|
||||
issuableType,
|
||||
workspaceType: 'project',
|
||||
attrWorkspacePath: defaultProps.fullPath,
|
||||
labelCreateType: LabelType.project,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,7 +9,7 @@ import MockAdapter from 'axios-mock-adapter';
|
|||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createFlash from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
|
||||
import { sortMilestonesByDueDate } from '~/milestones/utils';
|
||||
|
||||
import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
|
||||
|
@ -17,7 +17,7 @@ import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/m
|
|||
import { mockMilestoneToken, mockMilestones, mockRegularMilestone } from '../mock_data';
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/milestones/milestone_utils');
|
||||
jest.mock('~/milestones/utils');
|
||||
|
||||
const defaultStubs = {
|
||||
Portal: true,
|
||||
|
|
|
@ -3205,11 +3205,21 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
|
||||
context 'when pipeline is not child nor parent' do
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, :created) }
|
||||
let_it_be(:build) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) }
|
||||
let_it_be(:build, refind: true) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) }
|
||||
|
||||
it 'returns just the pipeline environment' do
|
||||
expect(subject).to contain_exactly(build.deployment.environment)
|
||||
end
|
||||
|
||||
context 'when deployment SHA is not matched' do
|
||||
before do
|
||||
build.deployment.update!(sha: 'old-sha')
|
||||
end
|
||||
|
||||
it 'does not return environments' do
|
||||
expect(subject).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an associated environment does not have deployments' do
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.shared_examples '.find_by_full_path' do
|
||||
RSpec.shared_examples 'routable resource' do
|
||||
describe '.find_by_full_path', :aggregate_failures do
|
||||
it 'finds records by their full path' do
|
||||
expect(described_class.find_by_full_path(record.full_path)).to eq(record)
|
||||
|
@ -52,13 +52,27 @@ RSpec.shared_examples '.find_by_full_path' do
|
|||
end
|
||||
end
|
||||
|
||||
RSpec.describe Routable do
|
||||
it_behaves_like '.find_by_full_path' do
|
||||
let_it_be(:record) { create(:group) }
|
||||
RSpec.shared_examples 'routable resource with parent' do
|
||||
it_behaves_like 'routable resource'
|
||||
|
||||
describe '#full_path' do
|
||||
it { expect(record.full_path).to eq "#{record.parent.full_path}/#{record.path}" }
|
||||
|
||||
it 'hits the cache when not preloaded' do
|
||||
forcibly_hit_cached_lookup(record, :full_path)
|
||||
|
||||
expect(record.full_path).to eq("#{record.parent.full_path}/#{record.path}")
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like '.find_by_full_path' do
|
||||
let_it_be(:record) { create(:project) }
|
||||
describe '#full_name' do
|
||||
it { expect(record.full_name).to eq "#{record.parent.human_name} / #{record.name}" }
|
||||
|
||||
it 'hits the cache when not preloaded' do
|
||||
forcibly_hit_cached_lookup(record, :full_name)
|
||||
|
||||
expect(record.full_name).to eq("#{record.parent.human_name} / #{record.name}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -66,6 +80,14 @@ RSpec.describe Group, 'Routable', :with_clean_rails_cache do
|
|||
let_it_be_with_reload(:group) { create(:group, name: 'foo') }
|
||||
let_it_be(:nested_group) { create(:group, parent: group) }
|
||||
|
||||
it_behaves_like 'routable resource' do
|
||||
let_it_be(:record) { group }
|
||||
end
|
||||
|
||||
it_behaves_like 'routable resource with parent' do
|
||||
let_it_be(:record) { nested_group }
|
||||
end
|
||||
|
||||
describe 'Validations' do
|
||||
it { is_expected.to validate_presence_of(:route) }
|
||||
end
|
||||
|
@ -119,24 +141,6 @@ RSpec.describe Group, 'Routable', :with_clean_rails_cache do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.find_by_full_path' do
|
||||
it_behaves_like '.find_by_full_path' do
|
||||
let_it_be(:record) { group }
|
||||
end
|
||||
|
||||
it_behaves_like '.find_by_full_path' do
|
||||
let_it_be(:record) { nested_group }
|
||||
end
|
||||
|
||||
it 'does not find projects with a matching path' do
|
||||
project = create(:project)
|
||||
redirect_route = create(:redirect_route, source: project)
|
||||
|
||||
expect(described_class.find_by_full_path(project.full_path)).to be_nil
|
||||
expect(described_class.find_by_full_path(redirect_route.path, follow_redirects: true)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '.where_full_path_in' do
|
||||
context 'without any paths' do
|
||||
it 'returns an empty relation' do
|
||||
|
@ -195,64 +199,39 @@ RSpec.describe Group, 'Routable', :with_clean_rails_cache do
|
|||
expect(group.route_loaded?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#full_path' do
|
||||
it { expect(group.full_path).to eq(group.path) }
|
||||
it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") }
|
||||
|
||||
it 'hits the cache when not preloaded' do
|
||||
forcibly_hit_cached_lookup(nested_group, :full_path)
|
||||
|
||||
expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#full_name' do
|
||||
it { expect(group.full_name).to eq(group.name) }
|
||||
it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
|
||||
|
||||
it 'hits the cache when not preloaded' do
|
||||
forcibly_hit_cached_lookup(nested_group, :full_name)
|
||||
|
||||
expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe Project, 'Routable', :with_clean_rails_cache do
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:project) { create(:project, namespace: namespace) }
|
||||
|
||||
it_behaves_like '.find_by_full_path' do
|
||||
it_behaves_like 'routable resource with parent' do
|
||||
let_it_be(:record) { project }
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not find groups with a matching path' do
|
||||
group = create(:group)
|
||||
redirect_route = create(:redirect_route, source: group)
|
||||
|
||||
expect(described_class.find_by_full_path(group.full_path)).to be_nil
|
||||
expect(described_class.find_by_full_path(redirect_route.path, follow_redirects: true)).to be_nil
|
||||
end
|
||||
|
||||
describe '#full_path' do
|
||||
it { expect(project.full_path).to eq "#{namespace.full_path}/#{project.path}" }
|
||||
|
||||
it 'hits the cache when not preloaded' do
|
||||
forcibly_hit_cached_lookup(project, :full_path)
|
||||
|
||||
expect(project.full_path).to eq("#{namespace.full_path}/#{project.path}")
|
||||
RSpec.describe Namespaces::ProjectNamespace, 'Routable', :with_clean_rails_cache do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project_namespace) do
|
||||
# For now we create only project namespace w/o project, otherwise same path
|
||||
# would be used for project and project namespace.
|
||||
# This can be removed when route is created automatically for project namespaces.
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/346448
|
||||
create(:project_namespace, project: nil, parent: group,
|
||||
visibility_level: Gitlab::VisibilityLevel::PUBLIC,
|
||||
path: 'foo', name: 'foo').tap do |project_namespace|
|
||||
Route.create!(source: project_namespace, path: project_namespace.full_path,
|
||||
name: project_namespace.full_name)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#full_name' do
|
||||
it { expect(project.full_name).to eq "#{namespace.human_name} / #{project.name}" }
|
||||
# we have couple of places where we use generic Namespace, in that case
|
||||
# we don't want to include ProjectNamespace routes yet
|
||||
it 'ignores project namespace when searching for generic namespace' do
|
||||
redirect_route = create(:redirect_route, source: project_namespace)
|
||||
|
||||
it 'hits the cache when not preloaded' do
|
||||
forcibly_hit_cached_lookup(project, :full_name)
|
||||
|
||||
expect(project.full_name).to eq("#{namespace.human_name} / #{project.name}")
|
||||
end
|
||||
expect(Namespace.find_by_full_path(project_namespace.full_path)).to be_nil
|
||||
expect(Namespace.find_by_full_path(redirect_route.path, follow_redirects: true)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue