Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-11-30 15:15:02 +00:00
parent 3bba41a8c5
commit d0713b8075
113 changed files with 978 additions and 1970 deletions

View File

@ -774,11 +774,19 @@ rspec-ee unit gitlab-duo-chat-zeroshot pg14:
rspec-ee unit gitlab-duo-chat-qa-fast pg14:
extends:
- .rspec-ee-base-gitlab-duo
- .rails:rules:ee-gitlab-duo-chat-qa-fast
- .rails:rules:ee-gitlab-duo-chat-always
script:
- !reference [.base-script, script]
- rspec_paralellized_job "--tag fast_chat_qa_evaluation"
rspec-ee unit gitlab-duo pg14:
extends:
- .rspec-ee-base-gitlab-duo
- .rails:rules:ee-gitlab-duo-chat-always
script:
- !reference [.base-script, script]
- rspec_paralellized_job "--tag gitlab_duo"
rspec-ee unit gitlab-duo-chat-qa pg14:
variables:
QA_EVAL_REPORT_FILENAME: "qa_evaluation_report.md"

View File

@ -85,13 +85,27 @@ include:
- bundle exec gem list gitlab_quality-test_tooling
- |
if [ "$CREATE_RAILS_TEST_FAILURE_ISSUES" == "true" ]; then
bundle exec relate-failure-issue --input-files "rspec/rspec-*.json" --system-log-files "log" --project "gitlab-org/gitlab" --token "${TEST_FAILURES_PROJECT_TOKEN}" --related-issues-file "rspec/${CI_JOB_ID}-failed-test-issues.json";
bundle exec relate-failure-issue \
--token "${TEST_FAILURES_PROJECT_TOKEN}" \
--project "gitlab-org/gitlab" \
--input-files "rspec/rspec-*.json" \
--exclude-labels-for-search "QA,rspec:slow test" \
--system-log-files "log" \
--related-issues-file "rspec/${CI_JOB_ID}-failed-test-issues.json";
fi
if [ "$CREATE_RAILS_SLOW_TEST_ISSUES" == "true" ]; then
bundle exec slow-test-issues --input-files "rspec/rspec-*.json" --project "gitlab-org/gitlab" --token "${TEST_FAILURES_PROJECT_TOKEN}" --related-issues-file "rspec/${CI_JOB_ID}-slow-test-issues.json";
bundle exec slow-test-issues \
--token "${TEST_FAILURES_PROJECT_TOKEN}" \
--project "gitlab-org/gitlab" \
--input-files "rspec/rspec-*.json" \
--related-issues-file "rspec/${CI_JOB_ID}-slow-test-issues.json";
fi
if [ "$ADD_SLOW_TEST_NOTE_TO_MERGE_REQUEST" == "true" ]; then
bundle exec slow-test-merge-request-report-note --input-files "rspec/rspec-*.json" --project "gitlab-org/gitlab" --merge_request_iid "$CI_MERGE_REQUEST_IID" --token "${TEST_SLOW_NOTE_PROJECT_TOKEN}";
bundle exec slow-test-merge-request-report-note \
--token "${TEST_SLOW_NOTE_PROJECT_TOKEN}" \
--project "gitlab-org/gitlab" \
--input-files "rspec/rspec-*.json" \
--merge_request_iid "$CI_MERGE_REQUEST_IID";
fi
- echo -e "\e[0Ksection_end:`date +%s`:report_results_section\r\e[0K"
- tooling/bin/push_job_metrics || true

View File

@ -372,6 +372,7 @@
.ai-patterns: &ai-patterns
- "{,ee/,jh/}lib/gitlab/llm/**/*"
- "{,ee/,jh/}{,spec/}lib/gitlab/llm/**/*"
- "{,ee/,jh/}lib/gitlab/duo/**/*"
# DB patterns + .ci-patterns
.db-patterns: &db-patterns
@ -2166,7 +2167,7 @@
when: manual
allow_failure: true
.rails:rules:ee-gitlab-duo-chat-qa-fast:
.rails:rules:ee-gitlab-duo-chat-always:
rules:
- !reference [".rails:rules:ee-gitlab-duo-chat-base", rules]
- <<: *if-merge-request

View File

@ -326,7 +326,6 @@ Gitlab/NamespacedClass:
- 'app/models/user_custom_attribute.rb'
- 'app/models/user_detail.rb'
- 'app/models/user_highest_role.rb'
- 'app/models/user_interacted_project.rb'
- 'app/models/user_mention.rb'
- 'app/models/user_preference.rb'
- 'app/models/user_status.rb'

View File

@ -4754,7 +4754,6 @@ RSpec/FeatureCategory:
- 'spec/models/user_custom_attribute_spec.rb'
- 'spec/models/user_detail_spec.rb'
- 'spec/models/user_highest_role_spec.rb'
- 'spec/models/user_interacted_project_spec.rb'
- 'spec/models/user_mentions/commit_user_mention_spec.rb'
- 'spec/models/user_mentions/issue_user_mention_spec.rb'
- 'spec/models/user_mentions/merge_request_user_mention_spec.rb'

View File

@ -2858,7 +2858,6 @@ RSpec/NamedSubject:
- 'spec/models/uploads/fog_spec.rb'
- 'spec/models/uploads/local_spec.rb'
- 'spec/models/user_custom_attribute_spec.rb'
- 'spec/models/user_interacted_project_spec.rb'
- 'spec/models/user_spec.rb'
- 'spec/models/user_status_spec.rb'
- 'spec/models/users/credit_card_validation_spec.rb'

View File

@ -1,6 +1,4 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
import { omit } from 'lodash';
import { refreshCurrentPage, queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
@ -33,11 +31,10 @@ export default {
'isGroupBoard',
'issuableType',
'boardType',
'isApolloBoard',
],
data() {
return {
boardListsApollo: {},
boardLists: {},
activeListId: '',
boardId: this.initialBoardId,
filterParams: { ...this.initialFilterParams },
@ -59,20 +56,14 @@ export default {
this.setActiveId('');
}
},
skip() {
return !this.isApolloBoard;
},
},
boardListsApollo: {
boardLists: {
query() {
return listsQuery[this.issuableType].query;
},
variables() {
return this.listQueryVariables;
},
skip() {
return !this.isApolloBoard;
},
update(data) {
const { lists } = data[this.boardType].board;
return formatBoardLists(lists);
@ -91,7 +82,6 @@ export default {
},
computed: {
...mapGetters(['isSidebarOpen']),
listQueryVariables() {
return {
...(this.isIssueBoard && {
@ -107,13 +97,10 @@ export default {
return (gon?.licensed_features?.swimlanes && this.isShowingEpicsSwimlanes) ?? false;
},
isAnySidebarOpen() {
if (this.isApolloBoard) {
return this.activeBoardItem?.id || this.activeListId;
}
return this.isSidebarOpen;
return this.activeBoardItem?.id || this.activeListId;
},
activeList() {
return this.activeListId ? this.boardListsApollo[this.activeListId] : undefined;
return this.activeListId ? this.boardLists[this.activeListId] : undefined;
},
formattedFilterParams() {
return filterVariables({
@ -134,7 +121,7 @@ export default {
},
methods: {
refetchLists() {
this.$apollo.queries.boardListsApollo.refetch();
this.$apollo.queries.boardLists.refetch();
},
setActiveId(id) {
this.activeListId = id;
@ -167,14 +154,14 @@ export default {
:add-column-form-visible="addColumnFormVisible"
:is-swimlanes-on="isSwimlanesOn"
:filter-params="formattedFilterParams"
:board-lists-apollo="boardListsApollo"
:board-lists="boardLists"
:apollo-error="error"
:list-query-variables="listQueryVariables"
@setActiveList="setActiveId"
@setAddColumnFormVisibility="addColumnFormVisible = $event"
/>
<board-settings-sidebar
v-if="!isApolloBoard || activeList"
v-if="activeList"
:list="activeList"
:list-id="activeListId"
:board-id="boardId"

View File

@ -8,8 +8,6 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { sortBy } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
@ -51,7 +49,6 @@ export default {
'isEpicBoard',
'issuableType',
'isGroupBoard',
'isApolloBoard',
],
props: {
item: {
@ -187,7 +184,6 @@ export default {
},
},
methods: {
...mapActions(['performSearch']),
setError,
isIndexLessThanlimit(index) {
return index < this.limitBeforeCounter;
@ -225,9 +221,6 @@ export default {
updateHistory({
url: `${filterPath}${filter}`,
});
if (!this.isApolloBoard) {
this.performSearch();
}
eventHub.$emit('updateTokens');
}
},

View File

@ -1,7 +1,5 @@
<script>
import { GlDisclosureDropdown } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState } from 'vuex';
import Tracking from '~/tracking';
import {
BOARD_CARD_MOVE_TO_POSITIONS_OPTIONS,
@ -15,7 +13,6 @@ export default {
GlDisclosureDropdown,
},
mixins: [Tracking.mixin()],
inject: ['isApolloBoard'],
props: {
item: {
type: Object,
@ -37,7 +34,6 @@ export default {
},
},
computed: {
...mapState(['pageInfoByListId']),
tracking() {
return {
category: 'boards:list',
@ -45,9 +41,6 @@ export default {
property: `type_card`,
};
},
listHasNextPage() {
return this.pageInfoByListId[this.list.id]?.hasNextPage;
},
itemIdentifier() {
return `${this.item.id}-${this.item.iid}-${this.index}`;
},
@ -59,7 +52,6 @@ export default {
},
},
methods: {
...mapActions(['moveItem']),
moveToStart() {
this.track('click_toggle_button', {
label: 'move_to_start',
@ -85,20 +77,7 @@ export default {
});
},
moveToPosition({ positionInList }) {
if (this.isApolloBoard) {
this.$emit('moveToPosition', positionInList);
} else {
this.moveItem({
itemId: this.item.id,
itemIid: this.item.iid,
itemPath: this.item.referencePath,
fromListId: this.list.id,
toListId: this.list.id,
positionInList,
atIndex: this.index,
allItemsLoadedInList: !this.listHasNextPage,
});
}
this.$emit('moveToPosition', positionInList);
},
selectMoveAction({ text }) {
if (text === BOARD_CARD_MOVE_TO_POSITIONS_START_OPTION) {

View File

@ -1,6 +1,4 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapActions, mapState } from 'vuex';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
import { isListDraggable } from '../boards_util';
import BoardList from './board_list.vue';
@ -10,7 +8,6 @@ export default {
BoardListHeader,
BoardList,
},
inject: ['isApolloBoard'],
props: {
list: {
type: Object,
@ -25,48 +22,21 @@ export default {
type: Object,
required: true,
},
highlightedListsApollo: {
highlightedLists: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
...mapState(['filterParams', 'highlightedLists']),
...mapGetters(['getBoardItemsByList']),
highlightedListsToUse() {
return this.isApolloBoard ? this.highlightedListsApollo : this.highlightedLists;
},
highlighted() {
return this.highlightedListsToUse.includes(this.list.id);
},
listItems() {
return this.isApolloBoard ? [] : this.getBoardItemsByList(this.list.id);
return this.highlightedLists.includes(this.list.id);
},
isListDraggable() {
return isListDraggable(this.list);
},
filtersToUse() {
return this.isApolloBoard ? this.filters : this.filterParams;
},
},
watch: {
filterParams: {
handler() {
if (!this.isApolloBoard && this.list.id && !this.list.collapsed) {
this.fetchItemsForList({ listId: this.list.id });
}
},
deep: true,
immediate: true,
},
'list.id': {
handler(id) {
if (!this.isApolloBoard && id) {
this.fetchItemsForList({ listId: this.list.id });
}
},
},
highlighted: {
handler(highlighted) {
if (highlighted) {
@ -78,9 +48,6 @@ export default {
immediate: true,
},
},
methods: {
...mapActions(['fetchItemsForList']),
},
};
</script>
@ -101,17 +68,11 @@ export default {
>
<board-list-header
:list="list"
:filter-params="filtersToUse"
:filter-params="filters"
:board-id="boardId"
@setActiveList="$emit('setActiveList', $event)"
/>
<board-list
ref="board-list"
:board-id="boardId"
:board-items="listItems"
:list="list"
:filter-params="filtersToUse"
/>
<board-list ref="board-list" :board-id="boardId" :list="list" :filter-params="filters" />
</div>
</div>
</template>

View File

@ -3,8 +3,6 @@ import { GlAlert } from '@gitlab/ui';
import { sortBy } from 'lodash';
import produce from 'immer';
import Draggable from 'vuedraggable';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions } from 'vuex';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import { s__ } from '~/locale';
import { defaultSortableOptions } from '~/sortable/constants';
@ -29,15 +27,7 @@ export default {
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert,
},
inject: [
'boardType',
'canAdminList',
'isIssueBoard',
'isEpicBoard',
'disabled',
'issuableType',
'isApolloBoard',
],
inject: ['boardType', 'canAdminList', 'isIssueBoard', 'isEpicBoard', 'disabled', 'issuableType'],
props: {
boardId: {
type: String,
@ -51,7 +41,7 @@ export default {
type: Boolean,
required: true,
},
boardListsApollo: {
boardLists: {
type: Object,
required: false,
default: () => {},
@ -77,12 +67,11 @@ export default {
};
},
computed: {
...mapState(['boardLists', 'error']),
boardListsById() {
return this.isApolloBoard ? this.boardListsApollo : this.boardLists;
return this.boardLists;
},
boardListsToUse() {
const lists = this.isApolloBoard ? this.boardListsApollo : this.boardLists;
const lists = this.boardLists;
return sortBy([...Object.values(lists)], 'position');
},
canDragColumns() {
@ -109,11 +98,10 @@ export default {
return this.canDragColumns ? options : {};
},
errorToDisplay() {
return this.apolloError || this.error || null;
return this.apolloError || null;
},
},
methods: {
...mapActions(['moveList', 'unsetError']),
afterFormEnters() {
const el = this.canDragColumns ? this.$refs.list.$el : this.$refs.list;
el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' });
@ -126,11 +114,7 @@ export default {
}, flashAnimationDuration);
},
dismissError() {
if (this.isApolloBoard) {
setError({ message: null, captureError: false });
} else {
this.unsetError();
}
setError({ message: null, captureError: false });
},
async updateListPosition({
item: {
@ -139,17 +123,6 @@ export default {
newIndex,
to: { children },
}) {
if (!this.isApolloBoard) {
this.moveList({
item: {
dataset: { listId: movedListId, draggableItemType },
},
newIndex,
to: { children },
});
return;
}
if (draggableItemType !== DraggableItemTypes.list) {
return;
}
@ -199,7 +172,7 @@ export default {
__typename: 'UpdateBoardListPayload',
errors: [],
list: {
...this.boardListsApollo[movedListId],
...this.boardLists[movedListId],
position: targetPosition,
},
},
@ -240,7 +213,7 @@ export default {
:board-id="boardId"
:list="list"
:filters="filterParams"
:highlighted-lists-apollo="highlightedLists"
:highlighted-lists="highlightedLists"
:data-draggable-item-type="$options.draggableItemTypes.list"
:class="{ 'gl-display-none! gl-sm-display-inline-block!': addColumnFormVisible }"
@setActiveList="$emit('setActiveList', $event)"

View File

@ -1,7 +1,5 @@
<script>
import { pickBy, isEmpty, mapValues } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
@ -31,7 +29,7 @@ export default {
search: __('Search'),
},
components: { FilteredSearch },
inject: ['initialFilterParams', 'isApolloBoard'],
inject: ['initialFilterParams'],
props: {
isSwimlanesOn: {
type: Boolean,
@ -353,7 +351,6 @@ export default {
eventHub.$off('updateTokens', this.updateTokens);
},
methods: {
...mapActions(['performSearch']),
formattedFilterParams() {
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
const filtersCopy = convertObjectPropsToCamelCase(rawFilterParams, {});
@ -374,11 +371,7 @@ export default {
replace: true,
});
if (this.isApolloBoard) {
this.$emit('setFilters', this.formattedFilterParams());
} else {
this.performSearch();
}
this.$emit('setFilters', this.formattedFilterParams());
},
getFilterParams(filters = []) {
const notFilters = filters.filter((item) => item.value.operator === '!=');

View File

@ -1,9 +1,6 @@
<script>
import { GlModal, GlAlert } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { visitUrl, updateHistory, getParameterByName } from '~/lib/utils/url_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import eventHub from '~/boards/eventhub';
import { formType } from '../constants';
@ -61,9 +58,6 @@ export default {
isProjectBoard: {
default: false,
},
isApolloBoard: {
default: false,
},
},
props: {
canAdminBoard: {
@ -184,7 +178,6 @@ export default {
}
},
methods: {
...mapActions(['setBoard']),
setError,
isFocusMode() {
return Boolean(document.querySelector('.content-wrapper > .js-focus-mode-board.is-focused'));
@ -227,23 +220,12 @@ export default {
} else {
try {
const board = await this.createOrUpdateBoard();
if (this.isApolloBoard) {
if (this.board.id) {
eventHub.$emit('updateBoard', board);
} else {
this.$emit('addBoard', board);
}
if (this.board.id) {
eventHub.$emit('updateBoard', board);
} else {
this.setBoard(board);
this.$emit('addBoard', board);
}
this.cancel();
if (!this.isApolloBoard) {
const param = getParameterByName('group_by')
? `?group_by=${getParameterByName('group_by')}`
: '';
updateHistory({ url: `${this.boardBaseUrl}/${getIdFromGraphQLId(board.id)}${param}` });
}
} catch (error) {
setError({ error, message: this.$options.i18n.saveErrorMessage });
} finally {

View File

@ -70,7 +70,8 @@ export default {
},
boardItems: {
type: Array,
required: true,
required: false, // This is temporary while we remove :apollo_board FF
default: () => [],
},
filterParams: {
type: Object,

View File

@ -8,14 +8,11 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState } from 'vuex';
import { isListDraggable } from '~/boards/boards_util';
import { isScopedLabel, parseBoolean } from '~/lib/utils/common_utils';
import { fetchPolicies } from '~/lib/graphql';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { n__, s__ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
import Tracking from '~/tracking';
import { TYPE_ISSUE } from '~/issues/constants';
import { formatDate } from '~/lib/utils/datetime_utility';
@ -23,8 +20,6 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql';
import AccessorUtilities from '~/lib/utils/accessor';
import {
inactiveId,
LIST,
ListType,
toggleFormEventPrefix,
updateListQueries,
@ -81,9 +76,6 @@ export default {
issuableType: {
default: TYPE_ISSUE,
},
isApolloBoard: {
default: false,
},
},
props: {
list: {
@ -106,7 +98,6 @@ export default {
},
},
computed: {
...mapState(['activeId']),
isLoggedIn() {
return Boolean(this.currentUserId);
},
@ -238,21 +229,12 @@ export default {
}
},
methods: {
...mapActions(['updateList', 'setActiveId', 'toggleListCollapsed']),
openSidebarSettings() {
if (this.activeId === inactiveId) {
sidebarEventHub.$emit('sidebar.closeAll');
}
if (this.isApolloBoard) {
this.$apollo.mutate({
mutation: setActiveBoardItemMutation,
variables: { boardItem: null },
});
this.$emit('setActiveList', this.list.id);
} else {
this.setActiveId({ id: this.list.id, sidebarType: LIST });
}
this.$apollo.mutate({
mutation: setActiveBoardItemMutation,
variables: { boardItem: null },
});
this.$emit('setActiveList', this.list.id);
this.track('click_button', { label: 'list_settings' });
},
@ -297,33 +279,29 @@ export default {
}
},
async updateListFunction(collapsed) {
if (this.isApolloBoard) {
try {
await this.$apollo.mutate({
mutation: updateListQueries[this.issuableType].mutation,
variables: {
listId: this.list.id,
collapsed,
},
optimisticResponse: {
updateBoardList: {
__typename: 'UpdateBoardListPayload',
errors: [],
list: {
...this.list,
collapsed,
},
try {
await this.$apollo.mutate({
mutation: updateListQueries[this.issuableType].mutation,
variables: {
listId: this.list.id,
collapsed,
},
optimisticResponse: {
updateBoardList: {
__typename: 'UpdateBoardListPayload',
errors: [],
list: {
...this.list,
collapsed,
},
},
});
} catch (error) {
setError({
error,
message: s__('Boards|An error occurred while updating the list. Please try again.'),
});
}
} else {
this.updateList({ listId: this.list.id, collapsed });
},
});
} catch (error) {
setError({
error,
message: s__('Boards|An error occurred while updating the list. Please try again.'),
});
}
},
/**
@ -337,17 +315,13 @@ export default {
return `${start} - ${due}`;
},
updateLocalCollapsedStatus(collapsed) {
if (this.isApolloBoard) {
this.$apollo.mutate({
mutation: toggleCollapsedMutations[this.issuableType].mutation,
variables: {
list: this.list,
collapsed,
},
});
} else {
this.toggleListCollapsed({ listId: this.list.id, collapsed });
}
this.$apollo.mutate({
mutation: toggleCollapsedMutations[this.issuableType].mutation,
variables: {
list: this.list,
collapsed,
},
});
},
},
};

View File

@ -1,6 +1,4 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import { getMilestone, formatIssueInput, getBoardQuery } from 'ee_else_ce/boards/boards_util';
import BoardNewIssueMixin from 'ee_else_ce/boards/mixins/board_new_issue';
@ -22,7 +20,7 @@ export default {
ProjectSelect,
},
mixins: [BoardNewIssueMixin],
inject: ['boardType', 'groupId', 'fullPath', 'isGroupBoard', 'isEpicBoard', 'isApolloBoard'],
inject: ['boardType', 'groupId', 'fullPath', 'isGroupBoard', 'isEpicBoard'],
props: {
list: {
type: Object,
@ -50,9 +48,6 @@ export default {
boardId: this.boardId,
};
},
skip() {
return !this.isApolloBoard;
},
update(data) {
const { board } = data.workspace;
return {
@ -69,7 +64,6 @@ export default {
},
},
computed: {
...mapGetters(['getBoardItemsByList']),
formEventPrefix() {
return toggleFormEventPrefix.issue;
},
@ -81,37 +75,19 @@ export default {
},
},
methods: {
...mapActions(['addListNewIssue']),
submit({ title }) {
const labels = this.list.label ? [this.list.label] : [];
const assignees = this.list.assignee ? [this.list.assignee] : [];
const milestone = getMilestone(this.list);
if (this.isApolloBoard) {
return this.addNewIssueToList({
issueInput: {
title,
labelIds: labels?.map((l) => l.id),
assigneeIds: assignees?.map((a) => a?.id),
milestoneId: milestone?.id,
projectPath: this.projectPath,
},
});
}
const firstItemId = this.getBoardItemsByList(this.list.id)[0]?.id;
return this.addListNewIssue({
list: this.list,
return this.addNewIssueToList({
issueInput: {
title,
labelIds: labels?.map((l) => l.id),
assigneeIds: assignees?.map((a) => a?.id),
milestoneId: milestone?.id,
projectPath: this.projectPath,
moveAfterId: firstItemId,
},
}).then(() => {
this.cancel();
});
},
addNewIssueToList({ issueInput }) {

View File

@ -31,7 +31,6 @@ export default {
'fullPath',
'boardType',
'isEpicBoard',
'isApolloBoard',
],
props: {
boardId: {
@ -63,9 +62,6 @@ export default {
boardId: this.boardId,
};
},
skip() {
return !this.isApolloBoard;
},
update(data) {
const { board } = data.workspace;
return {

View File

@ -1,7 +1,5 @@
<script>
import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormInput, GlLink } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapActions } from 'vuex';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@ -22,7 +20,7 @@ export default {
directives: {
autofocusonshow,
},
inject: ['fullPath', 'issuableType', 'isEpicBoard', 'isApolloBoard'],
inject: ['fullPath', 'issuableType', 'isEpicBoard'],
props: {
activeItem: {
type: Object,
@ -37,15 +35,11 @@ export default {
};
},
computed: {
...mapGetters(['activeBoardItem']),
item() {
return this.isApolloBoard ? this.activeItem : this.activeBoardItem;
},
pendingChangesStorageKey() {
return this.getPendingChangesKey(this.item);
return this.getPendingChangesKey(this.activeItem);
},
projectPath() {
const referencePath = this.item.referencePath || '';
const referencePath = this.activeItem.referencePath || '';
return referencePath.slice(0, referencePath.indexOf('#'));
},
validationState() {
@ -53,7 +47,7 @@ export default {
},
},
watch: {
item: {
activeItem: {
handler(updatedItem, formerItem) {
if (formerItem?.title !== this.title) {
localStorage.setItem(this.getPendingChangesKey(formerItem), this.title);
@ -66,7 +60,6 @@ export default {
},
},
methods: {
...mapActions(['setActiveItemTitle']),
getPendingChangesKey(item) {
if (!item) {
return '';
@ -92,16 +85,12 @@ export default {
}
},
cancel() {
this.title = this.item.title;
this.title = this.activeItem.title;
this.$refs.sidebarItem.collapse();
this.showChangesAlert = false;
localStorage.removeItem(this.pendingChangesStorageKey);
},
async setActiveBoardItemTitle() {
if (!this.isApolloBoard) {
await this.setActiveItemTitle({ title: this.title, projectPath: this.projectPath });
return;
}
const { fullPath, issuableType, isEpicBoard, title } = this;
const workspacePath = isEpicBoard
? { groupPath: fullPath }
@ -111,7 +100,7 @@ export default {
variables: {
input: {
...workspacePath,
iid: String(this.item.iid),
iid: String(this.activeItem.iid),
title,
},
},
@ -120,7 +109,7 @@ export default {
async setTitle() {
this.$refs.sidebarItem.collapse();
if (!this.title || this.title === this.item.title) {
if (!this.title || this.title === this.activeItem.title) {
return;
}
@ -130,14 +119,14 @@ export default {
localStorage.removeItem(this.pendingChangesStorageKey);
this.showChangesAlert = false;
} catch (e) {
this.title = this.item.title;
this.title = this.activeItem.title;
setError({ error: e, message: this.$options.i18n.updateTitleError });
} finally {
this.loading = false;
}
},
handleOffClick() {
if (this.title !== this.item.title) {
if (this.title !== this.activeItem.title) {
this.showChangesAlert = true;
localStorage.setItem(this.pendingChangesStorageKey, this.title);
} else {
@ -166,13 +155,13 @@ export default {
>
<template #title>
<span data-testid="item-title">
<gl-link class="gl-reset-color gl-hover-text-blue-800" :href="item.webUrl">
{{ item.title }}
<gl-link class="gl-reset-color gl-hover-text-blue-800" :href="activeItem.webUrl">
{{ activeItem.title }}
</gl-link>
</span>
</template>
<template #collapsed>
<span class="gl-text-gray-800">{{ item.referencePath }}</span>
<span class="gl-text-gray-800">{{ activeItem.referencePath }}</span>
</template>
<gl-alert v-if="showChangesAlert" variant="warning" class="gl-mb-5" :dismissible="false">
{{ $options.i18n.reviewYourChanges }}

View File

@ -24,7 +24,7 @@ const apolloProvider = new VueApollo({
function mountBoardApp(el) {
const { boardId, groupId, fullPath, rootPath } = el.dataset;
const isApolloBoard = window.gon?.features?.apolloBoards;
const isApolloBoard = true;
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });

View File

@ -19,6 +19,11 @@ export default {
required: true,
},
},
data() {
return {
linkedPipelines: null,
};
},
apollo: {
linkedPipelines: {
query: getLinkedPipelinesQuery,

View File

@ -50,12 +50,13 @@ export default {
'instanceId',
'isRotating',
'hasRotateError',
'rotateEndpoint',
]),
topAreaBaseClasses() {
return ['gl-display-flex', 'gl-flex-direction-column'];
},
canUserRotateToken() {
return this.rotateInstanceIdPath !== '';
return this.rotateEndpoint !== '';
},
shouldRenderPagination() {
return (

View File

@ -3,7 +3,7 @@
import { GlButton, GlModal, GlTooltipDirective } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale';
import { __ } from '~/locale';
import ListItem from './list_item.vue';
export default {
@ -55,11 +55,6 @@ export default {
},
},
computed: {
titleText() {
if (!this.title) return __('Changes');
return sprintf(__('%{title} changes'), { title: this.title });
},
filesLength() {
return this.fileList.length;
},
@ -84,7 +79,7 @@ export default {
<div class="ide-commit-list-container">
<header class="multi-file-commit-panel-header gl-display-flex gl-mb-0">
<div class="gl-display-flex gl-align-items-center flex-fill">
<strong> {{ titleText }} </strong>
<strong> {{ __('Changes') }} </strong>
<div class="gl-display-flex gl-ml-auto">
<gl-button
v-if="!stagedList"

View File

@ -13,12 +13,17 @@ const Tracking = Object.assign(Tracker, {
return {
computed: {
trackingCategory() {
const localCategory = this.tracking ? this.tracking.category : null;
// TODO: refactor to remove potentially undefined property
// https://gitlab.com/gitlab-org/gitlab/-/issues/432995
const localCategory = 'tracking' in this ? this.tracking.category : null;
return localCategory || opts.category;
},
trackingOptions() {
const options = addExperimentContext(opts);
return { ...options, ...this.tracking };
// TODO: refactor to remove potentially undefined property
// https://gitlab.com/gitlab-org/gitlab/-/issues/432995
const tracking = 'tracking' in this ? this.tracking : {};
return { ...options, ...tracking };
},
},
methods: {

View File

@ -4,6 +4,8 @@ import { sprintf } from '~/locale';
import { updateRepositorySize } from '~/api/projects_api';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import SectionedPercentageBar from '~/usage_quotas/components/sectioned_percentage_bar.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getCostFactoredProjectStorageStatistics from 'ee_else_ce/usage_quotas/storage/queries/cost_factored_project_storage.query.graphql';
import getProjectStorageStatistics from 'ee_else_ce/usage_quotas/storage/queries/project_storage.query.graphql';
import {
ERROR_MESSAGE,
@ -32,10 +34,15 @@ export default {
ProjectStorageDetail,
SectionedPercentageBar,
},
mixins: [glFeatureFlagsMixin()],
inject: ['projectPath'],
apollo: {
project: {
query: getProjectStorageStatistics,
query() {
return this.glFeatures?.displayCostFactoredStorageSizeOnProjectPages
? getCostFactoredProjectStorageStatistics
: getProjectStorageStatistics;
},
variables() {
return {
fullPath: this.projectPath,

View File

@ -0,0 +1,23 @@
query getCostFactoredProjectStorageStatistics($fullPath: ID!) {
project(fullPath: $fullPath) {
id
statisticsDetailsPaths {
containerRegistry
buildArtifacts
packages
repository
snippets
wiki
}
statistics {
containerRegistrySize
buildArtifactsSize
lfsObjectsSize
packagesSize
repositorySize
snippetsSize
storageSize
wikiSize
}
}
}

View File

@ -1,13 +1,5 @@
<script>
import {
GlFilteredSearch,
GlButtonGroup,
GlButton,
GlDropdown,
GlDropdownItem,
GlFormCheckbox,
GlTooltipDirective,
} from '@gitlab/ui';
import { GlFilteredSearch, GlSorting, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
import RecentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
@ -22,10 +14,7 @@ import { filterEmptySearchTerm, uniqueTokens } from './filtered_search_utils';
export default {
components: {
GlFilteredSearch,
GlButtonGroup,
GlButton,
GlDropdown,
GlDropdownItem,
GlSorting,
GlFormCheckbox,
},
directives: {
@ -118,8 +107,7 @@ export default {
recentSearchesPromise: null,
recentSearches: [],
filterValue: this.initialFilterValue,
selectedSortOption: this.sortOptions[0],
selectedSortDirection: SORT_DIRECTION.descending,
...this.getInitialSort(),
};
},
computed: {
@ -141,15 +129,14 @@ export default {
{},
);
},
sortDirectionIcon() {
return this.selectedSortDirection === SORT_DIRECTION.ascending
? 'sort-lowest'
: 'sort-highest';
transformedSortOptions() {
return this.sortOptions.map(({ id: value, title: text }) => ({ value, text }));
},
sortDirectionTooltip() {
return this.selectedSortDirection === SORT_DIRECTION.ascending
? __('Sort direction: Ascending')
: __('Sort direction: Descending');
selectedSortDirection() {
return this.sortDirectionAscending ? SORT_DIRECTION.ascending : SORT_DIRECTION.descending;
},
selectedSortOption() {
return this.sortOptions.find((sortOption) => sortOption.id === this.sortById);
},
/**
* This prop fixes a behaviour affecting GlFilteredSearch
@ -184,14 +171,13 @@ export default {
this.filterValue = newValue;
}
},
initialSortBy(newValue) {
if (this.syncFilterAndSort) {
this.updateSelectedSortValues(newValue);
initialSortBy(newInitialSortBy) {
if (this.syncFilterAndSort && newInitialSortBy) {
this.updateSelectedSortValues();
}
},
},
created() {
this.updateSelectedSortValues(this.initialSortBy);
if (this.recentSearchesStorageKey) this.setupRecentSearch();
},
methods: {
@ -273,15 +259,12 @@ export default {
return filter;
});
},
handleSortOptionClick(sortBy) {
this.selectedSortOption = sortBy;
this.$emit('onSort', sortBy.sortDirection[this.selectedSortDirection]);
handleSortByChange(sortById) {
this.sortById = sortById;
this.$emit('onSort', this.selectedSortOption.sortDirection[this.selectedSortDirection]);
},
handleSortDirectionClick() {
this.selectedSortDirection =
this.selectedSortDirection === SORT_DIRECTION.ascending
? SORT_DIRECTION.descending
: SORT_DIRECTION.ascending;
handleSortDirectionChange(isAscending) {
this.sortDirectionAscending = isAscending;
this.$emit('onSort', this.selectedSortOption.sortDirection[this.selectedSortDirection]);
},
handleHistoryItemSelected(filters) {
@ -328,18 +311,30 @@ export default {
const cleared = true;
this.$emit('onFilter', [], cleared);
},
updateSelectedSortValues(sort) {
if (!sort) {
return;
updateSelectedSortValues() {
Object.assign(this, this.getInitialSort());
},
getInitialSort() {
for (const sortOption of this.sortOptions) {
if (sortOption.sortDirection.ascending === this.initialSortBy) {
return {
sortById: sortOption.id,
sortDirectionAscending: true,
};
}
if (sortOption.sortDirection.descending === this.initialSortBy) {
return {
sortById: sortOption.id,
sortDirectionAscending: false,
};
}
}
this.selectedSortOption = this.sortOptions.find(
(sortBy) =>
sortBy.sortDirection.ascending === sort || sortBy.sortDirection.descending === sort,
);
this.selectedSortDirection = Object.keys(this.selectedSortOption?.sortDirection || {}).find(
(key) => this.selectedSortOption.sortDirection[key] === sort,
);
return {
sortById: this.sortOptions[0]?.id,
sortDirectionAscending: false,
};
},
},
};
@ -390,25 +385,14 @@ export default {
</template>
</template>
</gl-filtered-search>
<gl-button-group v-if="selectedSortOption" class="sort-dropdown-container d-flex">
<gl-dropdown :text="selectedSortOption.title" :right="true" class="w-100">
<gl-dropdown-item
v-for="sortBy in sortOptions"
:key="sortBy.id"
is-check-item
:is-checked="sortBy.id === selectedSortOption.id"
@click="handleSortOptionClick(sortBy)"
>{{ sortBy.title }}</gl-dropdown-item
>
</gl-dropdown>
<gl-button
v-gl-tooltip
:title="sortDirectionTooltip"
:aria-label="sortDirectionTooltip"
:icon="sortDirectionIcon"
class="flex-shrink-1"
@click="handleSortDirectionClick"
/>
</gl-button-group>
<gl-sorting
v-if="selectedSortOption"
:sort-options="transformedSortOptions"
:sort-by="sortById"
:is-ascending="sortDirectionAscending"
class="sort-dropdown-container"
@sortByChange="handleSortByChange"
@sortDirectionChange="handleSortDirectionChange"
/>
</div>
</template>

View File

@ -11,6 +11,7 @@ query groupWorkItems(
id
iid
title
confidential
}
}
}

View File

@ -13,6 +13,7 @@ query projectWorkItems(
id
iid
title
confidential
}
}
workItemsByIid: workItems(iid: $iid, types: $types) @include(if: $isNumber) {
@ -20,6 +21,7 @@ query projectWorkItems(
id
iid
title
confidential
}
}
}

View File

@ -9,6 +9,9 @@
@import './ide_themes/solarized-dark';
@import './ide_themes/monokai';
// This whole file is for the legacy Web IDE
// See: https://gitlab.com/groups/gitlab-org/-/epics/7683
$search-list-icon-width: 18px;
$ide-activity-bar-width: 60px;
$ide-context-header-padding: 10px;
@ -18,6 +21,10 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
$ide-commit-row-height: 32px;
$ide-commit-header-height: 48px;
.web-ide-loader {
padding-top: 1rem;
}
.project-refs-form,
.project-refs-target-form {
display: inline-block;
@ -517,7 +524,6 @@ $ide-commit-header-height: 48px;
.ide-empty-state {
display: flex;
height: 100vh;
align-items: center;
justify-content: center;
background-color: var(--ide-empty-state-background, transparent);
@ -526,6 +532,7 @@ $ide-commit-header-height: 48px;
.ide {
overflow: hidden;
flex: 1;
height: calc(100vh - var(--top-bar-height))
}
.ide-commit-list-container {

View File

@ -124,13 +124,6 @@
border-color: $gray-800;
}
.nav-sidebar,
.toggle-sidebar-button,
.close-nav-button {
background-color: darken($gray-50, 4%);
border-right: 1px solid $gray-50;
}
.gl-avatar:not(.gl-avatar-identicon),
.avatar-container,
.avatar {
@ -142,83 +135,18 @@
box-shadow: inset 0 0 0 1px rgba($gray-950, $gl-avatar-border-opacity);
}
.nav-sidebar {
.sidebar-sub-level-items.fly-out-list {
box-shadow: none;
border: 1px solid $border-color;
}
}
aside.right-sidebar:not(.right-sidebar-merge-requests) {
background-color: $gray-10;
border-left-color: $gray-50;
}
:root.gl-dark {
@include gitlab-theme($gray-900, $gray-400, $gray-500, $gray-900, $white);
.terms {
.logo-text {
fill: var(--black);
}
}
.navbar.navbar-gitlab {
background-color: var(--gray-50);
box-shadow: 0 1px 0 0 var(--gray-100);
.navbar-sub-nav,
.navbar-nav {
li {
> a:hover,
> a:focus,
> button:hover,
> button:focus {
color: var(--gl-text-color);
background-color: var(--gray-200);
}
}
li.active,
li.dropdown.show {
> a,
> button {
color: var(--gl-text-color);
background-color: var(--gray-200);
}
}
}
.header-search-form {
background-color: var(--gray-100) !important;
box-shadow: inset 0 0 0 1px var(--border-color) !important;
&:active,
&:hover {
background-color: var(--gray-100) !important;
box-shadow: inset 0 0 0 1px var(--blue-200) !important;
}
}
.search {
form {
background-color: var(--gray-100);
box-shadow: inset 0 0 0 1px var(--border-color);
&:active,
&:hover {
background-color: var(--gray-100);
box-shadow: inset 0 0 0 1px var(--blue-200);
}
.search-input {
color: var(--gl-text-color);
}
}
}
}
.md :not(pre.code) > code {
background-color: $gray-200;
}

View File

@ -2,14 +2,6 @@
:root {
&.ui-blue {
@include gitlab-theme(
$theme-blue-200,
$theme-blue-500,
$theme-blue-700,
$theme-blue-900,
$white
);
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-blue-50,

View File

@ -2,14 +2,6 @@
:root {
&.ui-gray {
@include gitlab-theme(
$gray-200,
$gray-300,
$gray-500,
$gray-900,
$white
);
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$gray-50,

View File

@ -2,14 +2,6 @@
:root {
&.ui-green {
@include gitlab-theme(
$theme-green-200,
$theme-green-500,
$theme-green-700,
$theme-green-900,
$white
);
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-green-50,

View File

@ -2,271 +2,6 @@
/**
* Styles the GitLab application with a specific color theme
*/
@mixin gitlab-theme(
$search-and-nav-links,
$accent,
$border-and-box-shadow,
$navbar-theme-color,
$navbar-theme-contrast-color
) {
// Set custom properties
--gl-theme-accent: #{$accent};
$search-and-nav-links-a20: rgba($search-and-nav-links, 0.2);
$search-and-nav-links-a30: rgba($search-and-nav-links, 0.3);
$search-and-nav-links-a40: rgba($search-and-nav-links, 0.4);
$search-and-nav-links-a80: rgba($search-and-nav-links, 0.8);
// Header
.navbar-gitlab:not(.super-sidebar-logged-out) {
background-color: $navbar-theme-color;
.navbar-collapse {
color: $search-and-nav-links;
}
.container-fluid {
.navbar-toggler {
border-left: 1px solid lighten($border-and-box-shadow, 10%);
color: $search-and-nav-links;
}
}
.navbar-sub-nav,
.navbar-nav {
> li {
> a,
> button {
&:hover,
&:focus {
background-color: $search-and-nav-links-a20;
}
}
&.active,
&.dropdown.show {
> a,
> button {
color: $navbar-theme-color;
background-color: $navbar-theme-contrast-color;
}
}
&.line-separator {
border-left: 1px solid $search-and-nav-links-a20;
}
}
}
.navbar-sub-nav {
color: $search-and-nav-links;
}
.nav {
> li {
color: $search-and-nav-links;
&.header-search {
color: $gray-900;
}
> a {
.notification-dot {
border: 2px solid $navbar-theme-color;
}
&.header-help-dropdown-toggle {
.notification-dot {
background-color: $search-and-nav-links;
}
}
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $search-and-nav-links;
}
}
&:hover,
&:focus {
@include media-breakpoint-up(sm) {
background-color: $search-and-nav-links-a20;
}
svg {
fill: currentColor;
}
.notification-dot {
will-change: border-color, background-color;
border-color: adjust-color($navbar-theme-color, $red: 33, $green: 33, $blue: 33);
}
&.header-help-dropdown-toggle .notification-dot {
background-color: $white;
}
}
}
&.active > a,
&.dropdown.show > a {
color: $navbar-theme-color;
background-color: $navbar-theme-contrast-color;
&:hover {
svg {
fill: $navbar-theme-color;
}
}
.notification-dot {
border-color: $white;
}
&.header-help-dropdown-toggle {
.notification-dot {
background-color: $navbar-theme-color;
}
}
}
.impersonated-user,
.impersonated-user:hover {
svg {
fill: $navbar-theme-color;
}
}
}
}
}
.navbar .title {
> a {
&:hover,
&:focus {
background-color: $search-and-nav-links-a20;
}
}
}
.header-search-form {
background-color: $search-and-nav-links-a20 !important;
border-radius: $border-radius-default;
&:hover {
background-color: $search-and-nav-links-a30 !important;
}
&.is-focused {
input {
background-color: $white;
color: $gl-text-color !important;
box-shadow: inset 0 0 0 1px $gray-900;
&:focus {
box-shadow: inset 0 0 0 1px $gray-900, 0 0 0 1px $white, 0 0 0 3px $blue-400;
}
&::placeholder {
color: $gray-400;
}
}
}
svg.gl-search-box-by-type-search-icon {
color: $search-and-nav-links-a80;
}
input {
background-color: transparent;
color: $search-and-nav-links-a80;
box-shadow: inset 0 0 0 1px $search-and-nav-links-a40;
&::placeholder {
color: $search-and-nav-links-a80;
}
&:focus,
&:active {
&::placeholder {
color: $gray-400;
}
}
}
.keyboard-shortcut-helper {
color: $search-and-nav-links;
background-color: $search-and-nav-links-a20;
}
}
.search {
form {
background-color: $search-and-nav-links-a20;
&:hover {
background-color: $search-and-nav-links-a30;
}
}
.search-input::placeholder {
color: $search-and-nav-links-a80;
}
.search-input-wrap {
.search-icon,
.clear-icon {
fill: $search-and-nav-links-a80;
}
}
&.search-active {
form {
background-color: $white;
}
.search-input-wrap {
.search-icon {
fill: $search-and-nav-links-a80;
}
}
}
}
// Sidebar
.nav-sidebar li.active > a {
color: $gray-900;
}
.nav-sidebar {
.fly-out-top-item {
a,
a:hover,
&.active a,
.fly-out-top-item-container {
background-color: var(--gray-100, $gray-50);
color: var(--gray-900, $gray-900);
}
}
}
.branch-header-title {
color: $border-and-box-shadow;
}
.ide-sidebar-link {
&.active {
color: $border-and-box-shadow;
&.is-right {
box-shadow: inset -3px 0 $border-and-box-shadow;
}
}
}
}
@mixin gitlab-theme-super-sidebar(
$theme-color-lightest,
$theme-color-light,

View File

@ -2,14 +2,6 @@
:root {
&.ui-indigo {
@include gitlab-theme(
$theme-indigo-200,
$theme-indigo-500,
$theme-indigo-700,
$theme-indigo-900,
$white
);
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-indigo-50,

View File

@ -2,14 +2,6 @@
:root {
&.ui-light-blue {
@include gitlab-theme(
$theme-light-blue-200,
$theme-light-blue-500,
$theme-light-blue-500,
$theme-light-blue-700,
$white
);
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-light-blue-50,

View File

@ -1,102 +1,2 @@
@import './theme_helper';
:root {
&.ui-light-gray {
@include gitlab-theme(
$gray-500,
$gray-700,
$gray-500,
$gray-50,
$gray-500
);
.navbar-gitlab:not(.super-sidebar-logged-out) {
background-color: $gray-50;
box-shadow: 0 1px 0 0 $border-color;
.logo-text {
fill: #171321;
}
.navbar-sub-nav,
.navbar-nav {
> li {
> a:hover,
> a:focus,
> button:hover {
color: $gray-900;
}
&.active > a,
&.active > a:hover,
&.active > button {
color: $white;
}
> a,
> button {
&:active,
&:focus {
@include gl-focus;
}
}
}
}
.container-fluid {
.navbar-toggler,
.navbar-toggler:hover {
color: $gray-500;
border-left: 1px solid $gray-100;
}
}
}
.header-search-form {
background-color: $white !important;
box-shadow: inset 0 0 0 1px $border-color !important;
border-radius: $border-radius-default;
&:hover {
background-color: $white !important;
box-shadow: inset 0 0 0 1px $blue-200 !important;
}
}
.search {
form {
background-color: $white;
box-shadow: inset 0 0 0 1px $border-color;
&:hover {
background-color: $white;
box-shadow: inset 0 0 0 1px $blue-200;
}
}
.search-input-wrap {
.search-icon {
fill: $gray-100;
}
.search-input {
color: $gl-text-color;
}
}
}
.nav-sidebar li.active {
> a {
color: $gray-900;
}
svg {
fill: $gray-900;
}
}
.sidebar-top-level-items > li.active .badge.badge-pill {
color: $gray-900;
}
}
}
// "Light gray" is the default unthemed state of the sidebar.
// Nothing to do here.

View File

@ -2,14 +2,6 @@
:root {
&.ui-light-green {
@include gitlab-theme(
$theme-green-200,
$theme-green-500,
$theme-green-500,
$theme-green-700,
$white
);
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-green-50,

View File

@ -2,14 +2,6 @@
:root {
&.ui-light-indigo {
@include gitlab-theme(
$theme-indigo-200,
$theme-indigo-500,
$theme-indigo-500,
$theme-indigo-700,
$white
);
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-indigo-50,

View File

@ -2,14 +2,6 @@
:root {
&.ui-light-red {
@include gitlab-theme(
$theme-light-red-200,
$theme-light-red-500,
$theme-light-red-500,
$theme-light-red-700,
$white
);
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-light-red-50,

View File

@ -2,14 +2,6 @@
:root {
&.ui-red {
@include gitlab-theme(
$theme-red-200,
$theme-red-500,
$theme-red-700,
$theme-red-900,
$white
);
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-red-50,

View File

@ -11,7 +11,6 @@ module ExternalRedirect
redirect_to url_param
else
render layout: 'fullscreen', locals: {
minimal: true,
url: url_param
}
end

View File

@ -24,7 +24,7 @@ class IdeController < ApplicationController
@fork_info = fork_info(project, params[:branch])
end
render layout: 'fullscreen', locals: { minimal: helpers.use_new_web_ide? }
render layout: helpers.use_new_web_ide? ? 'fullscreen' : 'application'
end
private

View File

@ -17,7 +17,7 @@ module WebIde
def index
return render_404 unless Feature.enabled?(:vscode_web_ide, current_user)
render layout: 'fullscreen', locals: { minimal: true, data: root_element_data }
render layout: 'fullscreen', locals: { data: root_element_data }
end
private

View File

@ -79,7 +79,6 @@ class Event < ApplicationRecord
# Callbacks
after_create :reset_project_activity
after_create :set_last_repository_updated_at, if: :push_action?
after_create ->(event) { UserInteractedProject.track(event) }
# Scopes
scope :recent, -> { reorder(id: :desc) }

View File

@ -406,7 +406,7 @@ class Group < Namespace
end
def visibility_level_allowed_by_projects?(level = self.visibility_level)
!projects.without_deleted.where('visibility_level > ?', level).exists?
!projects.not_aimed_for_deletion.where('visibility_level > ?', level).exists?
end
def visibility_level_allowed_by_sub_groups?(level = self.visibility_level)

View File

@ -220,9 +220,6 @@ class User < MainClusterwide::ApplicationRecord
has_many :project_authorizations, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :user_interacted_projects
has_many :project_interactions, through: :user_interacted_projects, source: :project, class_name: 'Project'
has_many :snippets, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :notes, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :issues, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent

View File

@ -1,39 +0,0 @@
# frozen_string_literal: true
class UserInteractedProject < ApplicationRecord
extend SuppressCompositePrimaryKeyWarning
belongs_to :user
belongs_to :project
validates :project_id, presence: true
validates :user_id, presence: true
CACHE_EXPIRY_TIME = 1.day
class << self
def track(event)
# For events without a project, we simply don't care.
# An example of this is the creation of a snippet (which
# is not related to any project).
return unless event.project_id
attributes = {
project_id: event.project_id,
user_id: event.author_id
}
cached_exists?(**attributes) do
where(attributes).exists? || UserInteractedProject.insert_all([attributes], unique_by: %w[project_id user_id])
true
end
end
private
def cached_exists?(project_id:, user_id:, &block)
cache_key = "user_interacted_projects:#{project_id}:#{user_id}"
Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRY_TIME, &block)
end
end
end

View File

@ -1,7 +1,15 @@
- page_title _("IDE"), @project.full_name
- add_page_specific_style 'page_bundles/web_ide_loader'
// The block below is for the Web IDE
// See: https://gitlab.com/groups/gitlab-org/-/epics/7683
- unless use_new_web_ide?
- @breadcrumb_title = _("IDE")
- @breadcrumb_link = '#'
- @no_container = true
- @content_wrapper_class = 'pb-0'
- add_to_breadcrumbs(s_('Navigation|Your work'), root_path)
- nav 'your_work' # Couldn't get the `project` nav to work easily
- add_page_specific_style 'page_bundles/build'
- add_page_specific_style 'page_bundles/ide'

View File

@ -1,13 +1,9 @@
- minimal = local_assigns.fetch(:minimal, false)
!!! 5
%html{ class: [user_application_theme, page_class], lang: I18n.locale }
= render "layouts/head"
%body{ class: "#{user_tab_width} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
= render 'peek/bar'
= header_message
- unless minimal
= render partial: "layouts/header/default", locals: { project: @project, group: @group }
.mobile-overlay
.hide-when-top-nav-responsive-open.gl--flex-full.gl-h-full{ class: nav ? ["layout-page", page_with_sidebar_class, "gl-mt-0!"]: '' }
- if defined?(nav) && nav
= render "layouts/nav/sidebar/#{nav}"
@ -19,6 +15,4 @@
= render "layouts/flash", flash_container_no_margin: true
.content-wrapper{ id: "content-body", class: "d-flex flex-column align-items-stretch gl-p-0" }
= yield
- unless minimal
= render "layouts/nav/top_nav_responsive", class: "gl-flex-grow-1 gl-overflow-y-auto"
= footer_message

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/432219
milestone: '16.7'
type: development
group: group::pipeline authoring
default_enabled: false
default_enabled: true

View File

@ -302,6 +302,10 @@ pages_deployments:
- table: p_ci_builds
column: ci_build_id
on_delete: async_nullify
project_authorizations:
- table: users
column: user_id
on_delete: async_delete
projects:
- table: organizations
column: organization_id

View File

@ -31,7 +31,7 @@ return if lines_with_testids.empty? && deprecated_qa_class.empty?
if lines_with_testids.any?
markdown(<<~MARKDOWN)
### Deprecated `testid` selectors
### `testid` selectors
The following changed lines in this MR contain `testid` selectors:

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddInstanceLevelAiBetaFeaturesEnabledToAppSettings < Gitlab::Database::Migration[2.2]
milestone '16.7'
def change
add_column :application_settings, :instance_level_ai_beta_features_enabled, :boolean, null: false, default: false
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class RemoveUsersProjectAuthorizationsUserIdFk < Gitlab::Database::Migration[2.2]
milestone '16.7'
disable_ddl_transaction!
FOREIGN_KEY_NAME = "fk_rails_11e7aa3ed9"
def up
with_lock_retries do
remove_foreign_key_if_exists(:project_authorizations, :users,
name: FOREIGN_KEY_NAME, reverse_lock_order: true)
end
end
def down
add_concurrent_foreign_key(:project_authorizations, :users,
name: FOREIGN_KEY_NAME, column: :user_id,
target_column: :id, on_delete: :cascade)
end
end

View File

@ -0,0 +1 @@
a567da73e9ecdf930ad89c68fba02e8b30aba9e8e460a00e0bf272067ca21409

View File

@ -0,0 +1 @@
187bf045979bb377e9999a260791075cab983eeda34db7ca3851720d6c5f79f9

View File

@ -12269,6 +12269,7 @@ CREATE TABLE application_settings (
pre_receive_secret_detection_enabled boolean DEFAULT false NOT NULL,
can_create_organization boolean DEFAULT true NOT NULL,
web_ide_oauth_application_id integer,
instance_level_ai_beta_features_enabled boolean DEFAULT false NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
@ -38409,9 +38410,6 @@ ALTER TABLE ONLY zoom_meetings
ALTER TABLE ONLY gpg_signatures
ADD CONSTRAINT fk_rails_11ae8cb9a7 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY project_authorizations
ADD CONSTRAINT fk_rails_11e7aa3ed9 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY pm_affected_packages
ADD CONSTRAINT fk_rails_1279c1b9a1 FOREIGN KEY (pm_advisory_id) REFERENCES pm_advisories(id) ON DELETE CASCADE;

View File

@ -182,43 +182,7 @@ This list is not exhaustive of work needed to be done.
### 4. Routing layer
The routing layer is meant to offer a consistent user experience where all Cells are presented under a single domain (for example, `gitlab.com`), instead of having to navigate to separate domains.
The user will be able to use `https://gitlab.com` to access Cell-enabled GitLab.
Depending on the URL access, it will be transparently proxied to the correct Cell that can serve this particular information.
For example:
- All requests going to `https://gitlab.com/users/sign_in` are randomly distributed to all Cells.
- All requests going to `https://gitlab.com/gitlab-org/gitlab/-/tree/master` are always directed to Cell 5, for example.
- All requests going to `https://gitlab.com/my-username/my-project` are always directed to Cell 1.
1. **Technology.**
We decide what technology the routing service is written in.
The choice is dependent on the best performing language, and the expected way and place of deployment of the routing layer.
If it is required to make the service multi-cloud it might be required to deploy it to the CDN provider.
Then the service needs to be written using a technology compatible with the CDN provider.
1. **Cell discovery.**
The routing service needs to be able to discover and monitor the health of all Cells.
1. **User can use single domain to interact with many Cells.**
The routing service will intelligently route all requests to Cells based on the resource being
accessed versus the Cell containing the data.
1. **Router endpoints classification.**
The stateless routing service will fetch and cache information about endpoints from one of the Cells.
We need to implement a protocol that will allow us to accurately describe the incoming request (its fingerprint), so it can be classified by one of the Cells, and the results of that can be cached.
We also need to implement a mechanism for negative cache and cache eviction.
1. **GraphQL and other ambiguous endpoints.**
Most endpoints have a unique sharding key: the Organization, which directly or indirectly (via a Group or Project) can be used to classify endpoints.
Some endpoints are ambiguous in their usage (they don't encode the sharding key), or the sharding key is stored deep in the payload.
In these cases, we need to decide how to handle endpoints like `/api/graphql`.
See [Cells: Routing Service](routing-service.md).
### 5. Cell deployment

View File

@ -0,0 +1,96 @@
---
stage: core platform
group: Tenant Scale
description: 'Cells: Routing Service'
---
# Cells: Routing Service
This document describes design goals and architecture of Routing Service
used by Cells. To better understand where the Routing Service fits
into architecture take a look at [Deployment Architecture](deployment-architecture.md).
## Goals
The routing layer is meant to offer a consistent user experience where all Cells are presented under a single domain (for example, `gitlab.com`), instead of having to navigate to separate domains.
The user will be able to use `https://gitlab.com` to access Cell-enabled GitLab.
Depending on the URL access, it will be transparently proxied to the correct Cell that can serve this particular information.
For example:
- All requests going to `https://gitlab.com/users/sign_in` are randomly distributed to all Cells.
- All requests going to `https://gitlab.com/gitlab-org/gitlab/-/tree/master` are always directed to Cell 5, for example.
- All requests going to `https://gitlab.com/my-username/my-project` are always directed to Cell 1.
1. **Technology.**
We decide what technology the routing service is written in.
The choice is dependent on the best performing language, and the expected way and place of deployment of the routing layer.
If it is required to make the service multi-cloud it might be required to deploy it to the CDN provider.
Then the service needs to be written using a technology compatible with the CDN provider.
1. **Cell discovery.**
The routing service needs to be able to discover and monitor the health of all Cells.
1. **User can use single domain to interact with many Cells.**
The routing service will intelligently route all requests to Cells based on the resource being
accessed versus the Cell containing the data.
1. **Router endpoints classification.**
The stateless routing service will fetch and cache information about endpoints from one of the Cells.
We need to implement a protocol that will allow us to accurately describe the incoming request (its fingerprint), so it can be classified by one of the Cells, and the results of that can be cached.
We also need to implement a mechanism for negative cache and cache eviction.
1. **GraphQL and other ambiguous endpoints.**
Most endpoints have a unique sharding key: the Organization, which directly or indirectly (via a Group or Project) can be used to classify endpoints.
Some endpoints are ambiguous in their usage (they don't encode the sharding key), or the sharding key is stored deep in the payload.
In these cases, we need to decide how to handle endpoints like `/api/graphql`.
1. **Small.**
The Routing Service is configuration-driven and rules-driven, and does not implement any business logic.
The maximum size of the project source code in initial phase is 1_000 lines without tests.
The reason for the hard limit is to make the Routing Service to not have any special logic,
and could be rewritten into any technology in a matter of a few days.
## Requirements
| Requirement | Description | Priority |
|---------------|-------------------------------------------------------------------|----------|
| Discovery | needs to be able to discover and monitor the health of all Cells. | high |
| Security | only authorized cells can be routed to | high |
| Single domain | e.g. GitLab.com | high |
| Caching | can cache routing information for performance | high |
| Low latency | small overhead for user requests | high |
| Path-based | can make routing decision based on path | high |
| Complexity | the routing service should be configuration-driven and small | high |
| Stateless | does not need database, Cells provide all routing information | medium |
| Secrets-based | can make routing decision based on secret (e.g. JWT) | medium |
| Observability | can use existing observability tooling | low |
| Self-managed | can be eventually used by [self-managed](goals.md#self-managed) | low |
| Regional | can route requests to different [regions](goals.md#regions) | low |
## Non-Goals
Not yet defined.
## Proposal
TBD
## Technology
TBD
## Alternatives
TBD
## Links
- [Cells - Routing: Technology](https://gitlab.com/groups/gitlab-org/-/epics/11002)
- [Classify endpoints](https://gitlab.com/gitlab-org/gitlab/-/issues/430330)

View File

@ -137,7 +137,7 @@ Pipelines for forks display with the **fork** badge in the parent project:
![Pipeline ran in fork](img/pipeline_fork_v13_7.png)
### Run pipelines in the parent project **(PREMIUM ALL)**
### Run pipelines in the parent project
Project members in the parent project can trigger a merge request pipeline
for a merge request submitted from a fork project. This pipeline:

View File

@ -125,6 +125,11 @@ The following CI jobs for GitLab project run the rspecs tagged with `real_ai_req
The job is always run and not allowed to fail. Although there's a chance that the QA test still might fail,
it is cheap and fast to run and intended to prevent a regression in the QA test helpers.
- `rspec-ee unit gitlab-duo pg14`:
This job runs tests to ensure that the GitLab Duo features are functional without running into system errors.
The job is always run and not allowed to fail.
This job does NOT conduct evaluations. The quality of the feature is tested in the other jobs such as QA jobs.
### Management of credentials and API keys for CI jobs
All API keys required to run the rspecs should be [masked](../../ci/variables/index.md#mask-a-cicd-variable)

View File

@ -50,8 +50,14 @@ All AI features are experimental.
## Test AI features locally
NOTE:
Use [this snippet](https://gitlab.com/gitlab-org/gitlab/-/snippets/2554994) for help automating the following section.
**One-line setup**
```shell
# Replace the <test-group-name> by the group name you want to enable GitLab Duo features. If the group doesn't exist, it creates a new one.
RAILS_ENV=development bundle exec rake gitlab:duo:setup['<test-group-name>']
```
**Manual way**
1. Enable the required general feature flags:

View File

@ -571,8 +571,8 @@ what if we slightly change the purpose of it? What if instead of retrieving all
projects with `visibility_level` 0 or 20, we retrieve those that a user
interacted with somehow?
Fortunately, GitLab has an answer for this, and it's a table called
`user_interacted_projects`. This table has the following schema:
Prior to GitLab 16.7, GitLab used a table named `user_interacted_projects` to track user interactions with projects.
This table had the following schema:
```sql
Table "public.user_interacted_projects"

View File

@ -18,6 +18,7 @@ data for features.
|-------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
| DevOps Adoption | `FILTER=devops_adoption bundle exec rake db:seed_fu` | [31_devops_adoption.rb](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/db/fixtures/development/31_devops_adoption.rb) |
| Value Streams Dashboard | `FILTER=cycle_analytics SEED_VSA=1 bundle exec rake db:seed_fu` | [17_cycle_analytics.rb](https://gitlab.com/gitlab-org/gitlab/-/blob/master/db/fixtures/development/17_cycle_analytics.rb) |
| Value Streams Dashboard overview counts | `FILTER=vsd_overview_counts SEED_VSD_COUNTS=1 bundle exec rake db:seed_fu` | [93_vsd_overview_counts.rb](https://gitlab.com/gitlab-org/gitlab/-/tree/master/ee/db/fixtures/development/93_vsd_overview_counts.rb) |
| Value Stream Analytics | `FILTER=customizable_cycle_analytics SEED_CUSTOMIZABLE_CYCLE_ANALYTICS=1 bundle exec rake db:seed_fu` | [30_customizable_cycle_analytics](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/db/fixtures/development/30_customizable_cycle_analytics.rb) |
| CI/CD analytics | `FILTER=ci_cd_analytics SEED_CI_CD_ANALYTICS=1 bundle exec rake db:seed_fu` | [38_ci_cd_analytics](https://gitlab.com/gitlab-org/gitlab/-/blob/master/db/fixtures/development/38_ci_cd_analytics.rb?ref_type=heads) |
| Contributions Analytics<br><br>Productivity Analytics<br><br>Code review Analytics<br><br>Merge Request Analytics | `FILTER=productivity_analytics SEED_PRODUCTIVITY_ANALYTICS=1 bundle exec rake db:seed_fu` | [90_productivity_analytics](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/db/fixtures/development/90_productivity_analytics.rb) |

View File

@ -19,7 +19,6 @@ module Gitlab
scope = args[:key]
# this logic cannot be placed in the NamespaceResolver due to N+1
scope = scope.without_project_namespaces if scope == Namespace
# `with_route` avoids an N+1 calculating full_path
scope = scope.where_full_path_in(full_paths)
scope.each do |model_instance|

View File

@ -1273,9 +1273,6 @@ msgstr ""
msgid "%{time} UTC"
msgstr ""
msgid "%{title} changes"
msgstr ""
msgid "%{totalCpu} (%{freeSpacePercentage}%{percentSymbol} free)"
msgstr ""

View File

@ -216,8 +216,8 @@ def add_test_to_specs(definition)
spec_test = <<-EOF.strip_heredoc.indent(2)
context 'with loose foreign key on #{definition.from_table}.#{definition.column}' do
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:#{definition.to_table.singularize}) }
let!(:model) { create(:#{definition.from_table.singularize}, #{definition.column.delete_suffix("_id").singularize}: parent) }
let_it_be(:parent) { create(:#{definition.to_table.singularize}) }
let_it_be(:model) { create(:#{definition.from_table.singularize}, #{definition.column.delete_suffix("_id").singularize}: parent) }
end
end
EOF

View File

@ -16,8 +16,6 @@ global:
image:
pullPolicy: Always
ingress:
annotations:
external-dns.alpha.kubernetes.io/ttl: 10
configureCertmanager: false
tls:
secretName: review-apps-tls

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe "Admin::AbuseReports", :js, feature_category: :insider_threat do
include Features::SortingHelpers
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
@ -79,7 +81,7 @@ RSpec.describe "Admin::AbuseReports", :js, feature_category: :insider_threat do
expect(report_rows[1].text).to include(report_text(open_report2))
# updated_at asc
sort_by 'Updated date'
sort_by 'Updated date', from: 'Created date'
expect(report_rows[0].text).to include(report_text(open_report2))
expect(report_rows[1].text).to include(report_text(open_report))
@ -120,7 +122,7 @@ RSpec.describe "Admin::AbuseReports", :js, feature_category: :insider_threat do
expect(report_rows[1].text).to include(report_text(open_report2))
# created_at desc
sort_by 'Created date'
sort_by 'Created date', from: 'Number of Reports'
expect(report_rows[0].text).to include(report_text(open_report2))
expect(report_rows[1].text).to include(aggregated_report_text(open_report, 2))
@ -131,7 +133,7 @@ RSpec.describe "Admin::AbuseReports", :js, feature_category: :insider_threat do
expect(report_rows[0].text).to include(aggregated_report_text(open_report, 2))
expect(report_rows[1].text).to include(report_text(open_report2))
sort_by 'Updated date'
sort_by 'Updated date', from: 'Created date'
# updated_at asc
expect(report_rows[0].text).to include(report_text(open_report2))
@ -193,14 +195,10 @@ RSpec.describe "Admin::AbuseReports", :js, feature_category: :insider_threat do
select_tokens(*tokens, submit: true, input_text: 'Filter reports')
end
def sort_by(sort)
def sort_by(sort, from: 'Number of Reports')
page.within('.vue-filtered-search-bar-container .sort-dropdown-container') do
page.find('.gl-dropdown-toggle').click
page.within('.dropdown-menu') do
click_button sort
wait_for_requests
end
pajamas_sort_by sort, from: from
wait_for_requests
end
end
end

View File

@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe "Admin Runners", feature_category: :runner_fleet do
include Features::SortingHelpers
include Features::RunnersHelpers
include Spec::Support::Helpers::ModalHelpers
@ -480,8 +481,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end
end
click_on 'Created date' # Open "sort by" dropdown
click_on 'Last contact'
pajamas_sort_by 'Last contact', from: 'Created date'
click_on 'Sort direction: Descending'
within_testid('runner-list') do

View File

@ -111,8 +111,7 @@ RSpec.describe 'Dashboard Issues filtering', :js, feature_category: :team_planni
end
it 'keeps sorting issues after visiting Projects Issues page' do
click_button 'Created date'
click_button 'Due date'
pajamas_sort_by 'Due date', from: 'Created date'
visit project_issues_path(project)

View File

@ -211,19 +211,19 @@ RSpec.describe 'Dashboard Merge Requests', :js, feature_category: :code_review_w
end
it 'shows sorted merge requests' do
pajamas_sort_by(s_('SortOptions|Created date'))
pajamas_sort_by(s_('SortOptions|Priority'), from: s_('SortOptions|Created date'))
visit merge_requests_dashboard_path(assignee_username: current_user.username)
expect(find('.issues-filters')).to have_content('Created date')
expect(find('.issues-filters')).to have_content(s_('SortOptions|Priority'))
end
it 'keeps sorting merge requests after visiting Projects MR page' do
pajamas_sort_by(s_('SortOptions|Created date'))
pajamas_sort_by(s_('SortOptions|Priority'), from: s_('SortOptions|Created date'))
visit project_merge_requests_path(project)
expect(find('.issues-filters')).to have_content('Created date')
expect(find('.issues-filters')).to have_content(s_('SortOptions|Priority'))
end
end

View File

@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe 'Group issues page', feature_category: :groups_and_projects do
include Features::SortingHelpers
include FilteredSearchHelpers
include DragTo
@ -180,8 +181,7 @@ RSpec.describe 'Group issues page', feature_category: :groups_and_projects do
end
def select_manual_sort
click_button 'Created date'
click_button 'Manual'
pajamas_sort_by 'Manual', from: 'Created date'
wait_for_requests
end

View File

@ -2,6 +2,7 @@
require 'spec_helper'
RSpec.describe 'Sort Issuable List', feature_category: :team_planning do
include Features::SortingHelpers
include ListboxHelpers
let(:project) { create(:project, :public) }
@ -195,8 +196,7 @@ RSpec.describe 'Sort Issuable List', feature_category: :team_planning do
it 'supports sorting in asc and desc order' do
visit_issues_with_state(project, 'opened')
click_button('Created date')
click_on('Updated date')
pajamas_sort_by 'Updated date', from: 'Created date'
expect(page).to have_css('.issue:first-child', text: last_updated_issuable.title)
expect(page).to have_css('.issue:last-child', text: first_updated_issuable.title)

View File

@ -3,6 +3,7 @@
require "spec_helper"
RSpec.describe "User sorts issues", feature_category: :team_planning do
include Features::SortingHelpers
include SortingHelper
include IssueHelpers
@ -46,8 +47,7 @@ RSpec.describe "User sorts issues", feature_category: :team_planning do
it 'sorts by popularity', :js do
visit(project_issues_path(project))
click_button 'Created date'
click_on 'Popularity'
pajamas_sort_by 'Popularity', from: 'Created date'
page.within(".issues-list") do
page.within("li.issue:nth-child(1)") do

View File

@ -23,7 +23,7 @@ RSpec.describe 'User sorts merge requests', :js, feature_category: :code_review_
end
it 'keeps the sort option' do
pajamas_sort_by(s_('SortOptions|Milestone'))
pajamas_sort_by(s_('SortOptions|Milestone'), from: s_('SortOptions|Created date'))
visit(merge_requests_dashboard_path(assignee_username: user.username))
@ -49,7 +49,7 @@ RSpec.describe 'User sorts merge requests', :js, feature_category: :code_review_
it 'separates remember sorting with issues', :js do
create(:issue, project: project)
pajamas_sort_by(s_('SortOptions|Milestone'))
pajamas_sort_by(s_('SortOptions|Milestone'), from: s_('SortOptions|Created date'))
visit(project_issues_path(project))
@ -66,7 +66,7 @@ RSpec.describe 'User sorts merge requests', :js, feature_category: :code_review_
end
it 'sorts by popularity' do
pajamas_sort_by(s_('SortOptions|Popularity'))
pajamas_sort_by(s_('SortOptions|Popularity'), from: s_('SortOptions|Created date'))
page.within('.mr-list') do
page.within('li.merge-request:nth-child(1)') do

View File

@ -133,6 +133,33 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do
expect(find('[data-testid="links-child"]')).to have_content(task.title)
end
end
context 'with confidential issue' do
let_it_be_with_reload(:issue) { create(:issue, :confidential, project: project) }
let_it_be(:task) { create(:work_item, :confidential, :task, project: project) }
it 'adds an existing child task', :aggregate_failures do
page.within('[data-testid="work-item-links"]') do
click_button 'Add'
click_button 'Existing task'
expect(page).to have_button('Add task', disabled: true)
find('[data-testid="work-item-token-select-input"]').set(task.title)
wait_for_all_requests
click_button task.title
expect(page).to have_button('Add task', disabled: false)
send_keys :escape
click_button('Add task')
wait_for_all_requests
expect(find('[data-testid="links-child"]')).to have_content(task.title)
end
end
end
end
context 'in work item metadata' do

View File

@ -25,8 +25,7 @@ RSpec.describe "User sorts things", :js do
visit(project_issues_path(project))
click_button s_('SortOptions|Created date')
click_button sort_option
pajamas_sort_by sort_option, from: s_('SortOptions|Created date')
visit(project_path(project))
visit(project_issues_path(project))
@ -39,7 +38,7 @@ RSpec.describe "User sorts things", :js do
visit(project_merge_requests_path(project))
pajamas_sort_by(sort_option)
pajamas_sort_by sort_option, from: s_('SortOptions|Created date')
visit(assigned_mrs_dashboard_path)

View File

@ -2,8 +2,6 @@ import { GlLabel, GlLoadingIcon } from '@gitlab/ui';
import { range } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@ -13,7 +11,6 @@ import BoardCardInner from '~/boards/components/board_card_inner.vue';
import isShowingLabelsQuery from '~/graphql_shared/client/is_showing_labels.query.graphql';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import eventHub from '~/boards/eventhub';
import defaultStore from '~/boards/stores';
import { TYPE_ISSUE } from '~/issues/constants';
import { updateHistory } from '~/lib/utils/url_utility';
import { mockLabelList, mockIssue, mockIssueFullPath, mockIssueDirectNamespace } from './mock_data';
@ -21,7 +18,6 @@ import { mockLabelList, mockIssue, mockIssueFullPath, mockIssueDirectNamespace }
jest.mock('~/lib/utils/url_utility');
jest.mock('~/boards/eventhub');
Vue.use(Vuex);
Vue.use(VueApollo);
describe('Board card component', () => {
@ -43,24 +39,12 @@ describe('Board card component', () => {
let wrapper;
let issue;
let list;
let store;
const findIssuableBlockedIcon = () => wrapper.findComponent(IssuableBlockedIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findHiddenIssueIcon = () => wrapper.findByTestId('hidden-icon');
const findWorkItemIcon = () => wrapper.findComponent(WorkItemTypeIcon);
const performSearchMock = jest.fn();
const createStore = () => {
store = new Vuex.Store({
actions: {
performSearch: performSearchMock,
},
state: defaultStore.state,
});
};
const mockApollo = createMockApollo();
const createWrapper = ({ props = {}, isGroupBoard = true } = {}) => {
@ -72,7 +56,6 @@ describe('Board card component', () => {
});
wrapper = mountExtended(BoardCardInner, {
store,
apolloProvider: mockApollo,
propsData: {
list,
@ -94,7 +77,6 @@ describe('Board card component', () => {
allowSubEpics: false,
issuableType: TYPE_ISSUE,
isGroupBoard,
isApolloBoard: false,
},
});
};
@ -108,14 +90,9 @@ describe('Board card component', () => {
weight: 1,
};
createStore();
createWrapper({ props: { item: issue, list } });
});
afterEach(() => {
store = null;
});
it('renders issue title', () => {
expect(wrapper.find('.board-card-title').text()).toContain(issue.title);
});
@ -159,7 +136,6 @@ describe('Board card component', () => {
});
it('does not render item reference path', () => {
createStore();
createWrapper({ isGroupBoard: false });
expect(wrapper.find('.board-card-number').text()).not.toContain(mockIssueDirectNamespace);
@ -460,10 +436,6 @@ describe('Board card component', () => {
expect(updateHistory).toHaveBeenCalledTimes(1);
});
it('dispatches performSearch vuex action', () => {
expect(performSearchMock).toHaveBeenCalledTimes(1);
});
it('emits updateTokens event', () => {
expect(eventHub.$emit).toHaveBeenCalledTimes(1);
expect(eventHub.$emit).toHaveBeenCalledWith('updateTokens');
@ -480,10 +452,6 @@ describe('Board card component', () => {
expect(updateHistory).not.toHaveBeenCalled();
});
it('does not dispatch performSearch vuex action', () => {
expect(performSearchMock).not.toHaveBeenCalled();
});
it('does not emit updateTokens event', () => {
expect(eventHub.$emit).not.toHaveBeenCalled();
});

View File

@ -1,8 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -15,34 +13,15 @@ import { rawIssue, boardListsQueryResponse } from '../mock_data';
describe('BoardApp', () => {
let wrapper;
let store;
let mockApollo;
const errorMessage = 'Failed to fetch lists';
const boardListQueryHandler = jest.fn().mockResolvedValue(boardListsQueryResponse);
const boardListQueryHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage));
Vue.use(Vuex);
Vue.use(VueApollo);
const createStore = ({ mockGetters = {} } = {}) => {
store = new Vuex.Store({
state: {},
actions: {
performSearch: jest.fn(),
},
getters: {
isSidebarOpen: () => true,
...mockGetters,
},
});
};
const createComponent = ({
isApolloBoard = false,
issue = rawIssue,
handler = boardListQueryHandler,
} = {}) => {
const createComponent = ({ issue = rawIssue, handler = boardListQueryHandler } = {}) => {
mockApollo = createMockApollo([[boardListsQuery, handler]]);
mockApollo.clients.defaultClient.cache.writeQuery({
query: activeBoardItemQuery,
@ -53,7 +32,6 @@ describe('BoardApp', () => {
wrapper = shallowMount(BoardApp, {
apolloProvider: mockApollo,
store,
provide: {
fullPath: 'gitlab-org',
initialBoardId: 'gid://gitlab/Board/1',
@ -62,69 +40,46 @@ describe('BoardApp', () => {
boardType: 'group',
isIssueBoard: true,
isGroupBoard: true,
isApolloBoard,
},
});
};
beforeEach(() => {
beforeEach(async () => {
cacheUpdates.setError = jest.fn();
createComponent({ isApolloBoard: true });
await nextTick();
});
afterEach(() => {
store = null;
it('fetches lists', () => {
expect(boardListQueryHandler).toHaveBeenCalled();
});
it("should have 'is-compact' class when sidebar is open", () => {
createStore();
createComponent();
it('should have is-compact class when a card is selected', () => {
expect(wrapper.classes()).toContain('is-compact');
});
it("should not have 'is-compact' class when sidebar is closed", () => {
createStore({ mockGetters: { isSidebarOpen: () => false } });
createComponent();
it('should not have is-compact class when no card is selected', async () => {
createComponent({ isApolloBoard: true, issue: {} });
await nextTick();
expect(wrapper.classes()).not.toContain('is-compact');
});
describe('Apollo boards', () => {
beforeEach(async () => {
createComponent({ isApolloBoard: true });
await nextTick();
});
it('refetches lists when updateBoard event is received', async () => {
jest.spyOn(eventHub, '$on').mockImplementation(() => {});
it('fetches lists', () => {
expect(boardListQueryHandler).toHaveBeenCalled();
});
createComponent({ isApolloBoard: true });
await waitForPromises();
it('should have is-compact class when a card is selected', () => {
expect(wrapper.classes()).toContain('is-compact');
});
expect(eventHub.$on).toHaveBeenCalledWith('updateBoard', wrapper.vm.refetchLists);
});
it('should not have is-compact class when no card is selected', async () => {
createComponent({ isApolloBoard: true, issue: {} });
await nextTick();
it('sets error on fetch lists failure', async () => {
createComponent({ isApolloBoard: true, handler: boardListQueryHandlerFailure });
expect(wrapper.classes()).not.toContain('is-compact');
});
await waitForPromises();
it('refetches lists when updateBoard event is received', async () => {
jest.spyOn(eventHub, '$on').mockImplementation(() => {});
createComponent({ isApolloBoard: true });
await waitForPromises();
expect(eventHub.$on).toHaveBeenCalledWith('updateBoard', wrapper.vm.refetchLists);
});
it('sets error on fetch lists failure', async () => {
createComponent({ isApolloBoard: true, handler: boardListQueryHandlerFailure });
await waitForPromises();
expect(cacheUpdates.setError).toHaveBeenCalled();
});
expect(cacheUpdates.setError).toHaveBeenCalled();
});
});

View File

@ -8,7 +8,7 @@ import {
BOARD_CARD_MOVE_TO_POSITIONS_END_OPTION,
} from '~/boards/constants';
import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
import { mockList, mockIssue2, mockIssue, mockIssue3, mockIssue4 } from 'jest/boards/mock_data';
import { mockList, mockIssue2 } from 'jest/boards/mock_data';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
Vue.use(Vuex);
@ -28,30 +28,8 @@ describe('Board Card Move to position', () => {
let wrapper;
let trackingSpy;
let store;
let dispatch;
const itemIndex = 1;
const createStoreOptions = () => {
const state = {
pageInfoByListId: {
'gid://gitlab/List/1': {},
'gid://gitlab/List/2': { hasNextPage: true },
},
};
const getters = {
getBoardItemsByList: () => () => [mockIssue, mockIssue2, mockIssue3, mockIssue4],
};
const actions = {
moveItem: jest.fn(),
};
return {
state,
getters,
actions,
};
};
const createComponent = (propsData, isApolloBoard = false) => {
wrapper = shallowMount(BoardCardMoveToPosition, {
store,
@ -73,7 +51,6 @@ describe('Board Card Move to position', () => {
};
beforeEach(() => {
store = new Vuex.Store(createStoreOptions());
createComponent();
});
@ -96,50 +73,6 @@ describe('Board Card Move to position', () => {
});
describe('Dropdown options', () => {
beforeEach(() => {
createComponent({ index: itemIndex });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
dispatch = jest.spyOn(store, 'dispatch').mockImplementation(() => {});
});
afterEach(() => {
unmockTracking();
});
it.each`
dropdownIndex | dropdownItem | trackLabel | positionInList
${0} | ${dropdownOptions[0]} | ${'move_to_start'} | ${0}
${1} | ${dropdownOptions[1]} | ${'move_to_end'} | ${-1}
`(
'on click of dropdown index $dropdownIndex with label $dropdownLabel should call moveItem action with tracking label $trackLabel',
async ({ dropdownIndex, dropdownItem, trackLabel, positionInList }) => {
await findMoveToPositionDropdown().vm.$emit('shown');
expect(findDropdownItemAtIndex(dropdownIndex).text()).toBe(dropdownItem.text);
await findMoveToPositionDropdown().vm.$emit('action', dropdownItem);
expect(trackingSpy).toHaveBeenCalledWith('boards:list', 'click_toggle_button', {
category: 'boards:list',
label: trackLabel,
property: 'type_card',
});
expect(dispatch).toHaveBeenCalledWith('moveItem', {
fromListId: mockList.id,
itemId: mockIssue2.id,
itemIid: mockIssue2.iid,
itemPath: mockIssue2.referencePath,
positionInList,
toListId: mockList.id,
allItemsLoadedInList: true,
atIndex: itemIndex,
});
},
);
});
describe('Apollo boards', () => {
beforeEach(() => {
createComponent({ index: itemIndex }, true);
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);

View File

@ -4,17 +4,15 @@ import { nextTick } from 'vue';
import { listObj } from 'jest/boards/mock_data';
import BoardColumn from '~/boards/components/board_column.vue';
import { ListType } from '~/boards/constants';
import { createStore } from '~/boards/stores';
describe('Board Column Component', () => {
let wrapper;
let store;
const initStore = () => {
store = createStore();
};
const createComponent = ({ listType = ListType.backlog, collapsed = false } = {}) => {
const createComponent = ({
listType = ListType.backlog,
collapsed = false,
highlightedLists = [],
} = {}) => {
const listMock = {
...listObj,
listType,
@ -27,14 +25,11 @@ describe('Board Column Component', () => {
}
wrapper = shallowMount(BoardColumn, {
store,
propsData: {
list: listMock,
boardId: 'gid://gitlab/Board/1',
filters: {},
},
provide: {
isApolloBoard: false,
highlightedLists,
},
});
};
@ -43,10 +38,6 @@ describe('Board Column Component', () => {
const isCollapsed = () => wrapper.classes('is-collapsed');
describe('Given different list types', () => {
beforeEach(() => {
initStore();
});
it('is expandable when List Type is `backlog`', () => {
createComponent({ listType: ListType.backlog });
@ -70,40 +61,11 @@ describe('Board Column Component', () => {
describe('highlighting', () => {
it('scrolls to column when highlighted', async () => {
createComponent();
store.state.highlightedLists.push(listObj.id);
createComponent({ highlightedLists: [listObj.id] });
await nextTick();
expect(wrapper.element.scrollIntoView).toHaveBeenCalled();
});
});
describe('on mount', () => {
beforeEach(() => {
initStore();
jest.spyOn(store, 'dispatch').mockImplementation();
});
describe('when list is collapsed', () => {
it('does not call fetchItemsForList when', async () => {
createComponent({ collapsed: true });
await nextTick();
expect(store.dispatch).toHaveBeenCalledTimes(0);
});
});
describe('when the list is not collapsed', () => {
it('calls fetchItemsForList when', async () => {
createComponent({ collapsed: false });
await nextTick();
expect(store.dispatch).toHaveBeenCalledWith('fetchItemsForList', { listId: 300 });
});
});
});
});

View File

@ -3,14 +3,11 @@ import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
import Draggable from 'vuedraggable';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import getters from 'ee_else_ce/boards/stores/getters';
import * as cacheUpdates from '~/boards/graphql/cache_updates';
import BoardColumn from '~/boards/components/board_column.vue';
import BoardContent from '~/boards/components/board_content.vue';
@ -27,11 +24,6 @@ import {
} from '../mock_data';
Vue.use(VueApollo);
Vue.use(Vuex);
const actions = {
moveList: jest.fn(),
};
describe('BoardContent', () => {
let wrapper;
@ -41,26 +33,9 @@ describe('BoardContent', () => {
const errorMessage = 'Failed to update list';
const updateListHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage));
const defaultState = {
isShowingEpicsSwimlanes: false,
boardLists: mockListsById,
error: undefined,
issuableType: 'issue',
};
const createStore = (state = defaultState) => {
return new Vuex.Store({
actions,
getters,
state,
});
};
const createComponent = ({
state,
props = {},
canAdminList = true,
isApolloBoard = false,
issuableType = 'issue',
isIssueBoard = true,
isEpicBoard = false,
@ -75,17 +50,13 @@ describe('BoardContent', () => {
data: boardListsQueryResponse.data,
});
const store = createStore({
...defaultState,
...state,
});
wrapper = shallowMount(BoardContent, {
apolloProvider: mockApollo,
propsData: {
boardId: 'gid://gitlab/Board/1',
filterParams: {},
isSwimlanesOn: false,
boardListsApollo: mockListsById,
boardLists: mockListsById,
listQueryVariables,
addColumnFormVisible: false,
...props,
@ -98,9 +69,7 @@ describe('BoardContent', () => {
isEpicBoard,
isGroupBoard: true,
disabled: false,
isApolloBoard,
},
store,
stubs: {
BoardContentSidebar: stubComponent(BoardContentSidebar, {
template: '<div></div>',
@ -114,13 +83,26 @@ describe('BoardContent', () => {
const findDraggable = () => wrapper.findComponent(Draggable);
const findError = () => wrapper.findComponent(GlAlert);
const moveList = () => {
const movableListsOrder = [mockLists[0].id, mockLists[1].id];
findDraggable().vm.$emit('end', {
item: { dataset: { listId: mockLists[0].id, draggableItemType: DraggableItemTypes.list } },
newIndex: 1,
to: {
children: movableListsOrder.map((listId) => ({ dataset: { listId } })),
},
});
};
beforeEach(() => {
cacheUpdates.setError = jest.fn();
});
describe('default', () => {
beforeEach(() => {
beforeEach(async () => {
createComponent();
await waitForPromises();
});
it('renders a BoardColumn component per list', () => {
@ -146,6 +128,40 @@ describe('BoardContent', () => {
it('does not show the "add column" form', () => {
expect(findBoardAddNewColumn().exists()).toBe(false);
});
it('reorders lists', async () => {
moveList();
await waitForPromises();
expect(updateListHandler).toHaveBeenCalled();
});
it('sets error on reorder lists failure', async () => {
createComponent({ handler: updateListHandlerFailure });
moveList();
await waitForPromises();
expect(cacheUpdates.setError).toHaveBeenCalled();
});
describe('when error is passed', () => {
beforeEach(async () => {
createComponent({ props: { apolloError: 'Error' } });
await waitForPromises();
});
it('displays error banner', () => {
expect(findError().exists()).toBe(true);
});
it('dismisses error', async () => {
findError().vm.$emit('dismiss');
await nextTick();
expect(cacheUpdates.setError).toHaveBeenCalledWith({ message: null, captureError: false });
});
});
});
describe('when issuableType is not issue', () => {
@ -178,67 +194,6 @@ describe('BoardContent', () => {
});
});
describe('when Apollo boards FF is on', () => {
const moveList = () => {
const movableListsOrder = [mockLists[0].id, mockLists[1].id];
findDraggable().vm.$emit('end', {
item: { dataset: { listId: mockLists[0].id, draggableItemType: DraggableItemTypes.list } },
newIndex: 1,
to: {
children: movableListsOrder.map((listId) => ({ dataset: { listId } })),
},
});
};
beforeEach(async () => {
createComponent({ isApolloBoard: true });
await waitForPromises();
});
it('renders a BoardColumn component per list', () => {
expect(wrapper.findAllComponents(BoardColumn)).toHaveLength(mockLists.length);
});
it('renders BoardContentSidebar', () => {
expect(wrapper.findComponent(BoardContentSidebar).exists()).toBe(true);
});
it('reorders lists', async () => {
moveList();
await waitForPromises();
expect(updateListHandler).toHaveBeenCalled();
});
it('sets error on reorder lists failure', async () => {
createComponent({ isApolloBoard: true, handler: updateListHandlerFailure });
moveList();
await waitForPromises();
expect(cacheUpdates.setError).toHaveBeenCalled();
});
describe('when error is passed', () => {
beforeEach(async () => {
createComponent({ isApolloBoard: true, props: { apolloError: 'Error' } });
await waitForPromises();
});
it('displays error banner', () => {
expect(findError().exists()).toBe(true);
});
it('dismisses error', async () => {
findError().vm.$emit('dismiss');
await nextTick();
expect(cacheUpdates.setError).toHaveBeenCalledWith({ message: null, captureError: false });
});
});
});
describe('when "add column" form is visible', () => {
beforeEach(() => {
createComponent({ props: { addColumnFormVisible: true } });

View File

@ -1,7 +1,4 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
import { updateHistory } from '~/lib/utils/url_utility';
import {
@ -20,9 +17,6 @@ import {
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import { createStore } from '~/boards/stores';
Vue.use(Vuex);
jest.mock('~/lib/utils/url_utility', () => ({
updateHistory: jest.fn(),
@ -32,7 +26,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('BoardFilteredSearch', () => {
let wrapper;
let store;
const tokens = [
{
icon: 'labels',
@ -63,15 +56,12 @@ describe('BoardFilteredSearch', () => {
];
const createComponent = ({ initialFilterParams = {}, props = {}, provide = {} } = {}) => {
store = createStore();
wrapper = shallowMount(BoardFilteredSearch, {
provide: {
initialFilterParams,
fullPath: '',
isApolloBoard: false,
...provide,
},
store,
propsData: {
...props,
tokens,
@ -84,8 +74,6 @@ describe('BoardFilteredSearch', () => {
describe('default', () => {
beforeEach(() => {
createComponent();
jest.spyOn(store, 'dispatch').mockImplementation();
});
it('passes the correct tokens to FilteredSearch', () => {
@ -93,12 +81,6 @@ describe('BoardFilteredSearch', () => {
});
describe('when onFilter is emitted', () => {
it('calls performSearch', () => {
findFilteredSearch().vm.$emit('onFilter', [{ value: { data: '' } }]);
expect(store.dispatch).toHaveBeenCalledWith('performSearch');
});
it('calls historyPushState', () => {
findFilteredSearch().vm.$emit('onFilter', [{ value: { data: 'searchQuery' } }]);
@ -109,6 +91,18 @@ describe('BoardFilteredSearch', () => {
});
});
});
it('emits setFilters and updates URL when onFilter is emitted', () => {
findFilteredSearch().vm.$emit('onFilter', [{ value: { data: '' } }]);
expect(updateHistory).toHaveBeenCalledWith({
title: '',
replace: true,
url: 'http://test.host/',
});
expect(wrapper.emitted('setFilters')).toHaveLength(1);
});
});
describe('when eeFilters is not empty', () => {
@ -130,8 +124,6 @@ describe('BoardFilteredSearch', () => {
describe('when searching', () => {
beforeEach(() => {
createComponent();
jest.spyOn(store, 'dispatch').mockImplementation();
});
it('sets the url params to the correct results', () => {
@ -151,7 +143,6 @@ describe('BoardFilteredSearch', () => {
findFilteredSearch().vm.$emit('onFilter', mockFilters);
expect(store.dispatch).toHaveBeenCalledWith('performSearch');
expect(updateHistory).toHaveBeenCalledWith({
title: '',
replace: true,
@ -198,56 +189,42 @@ describe('BoardFilteredSearch', () => {
});
});
describe('when Apollo boards FF is on', () => {
describe('when iteration is passed a wildcard value with a cadence id', () => {
const url = (arg) => `http://test.host/?iteration_id=${arg}&iteration_cadence_id=1349`;
beforeEach(() => {
createComponent({ provide: { isApolloBoard: true } });
createComponent();
});
it('emits setFilters and updates URL when onFilter is emitted', () => {
findFilteredSearch().vm.$emit('onFilter', [{ value: { data: '' } }]);
it.each([
['Current&1349', url('Current'), 'Current'],
['Any&1349', url('Any'), 'Any'],
])('sets the url param %s', (iterationParam, expected, wildCardId) => {
Object.defineProperty(window, 'location', {
writable: true,
value: new URL(expected),
});
const mockFilters = [
{ type: TOKEN_TYPE_ITERATION, value: { data: iterationParam, operator: '=' } },
];
findFilteredSearch().vm.$emit('onFilter', mockFilters);
expect(updateHistory).toHaveBeenCalledWith({
title: '',
replace: true,
url: 'http://test.host/',
url: expected,
});
expect(wrapper.emitted('setFilters')).toHaveLength(1);
});
describe('when iteration is passed a wildcard value with a cadence id', () => {
const url = (arg) => `http://test.host/?iteration_id=${arg}&iteration_cadence_id=1349`;
it.each([
['Current&1349', url('Current'), 'Current'],
['Any&1349', url('Any'), 'Any'],
])('sets the url param %s', (iterationParam, expected, wildCardId) => {
Object.defineProperty(window, 'location', {
writable: true,
value: new URL(expected),
});
const mockFilters = [
{ type: TOKEN_TYPE_ITERATION, value: { data: iterationParam, operator: '=' } },
];
findFilteredSearch().vm.$emit('onFilter', mockFilters);
expect(updateHistory).toHaveBeenCalledWith({
title: '',
replace: true,
url: expected,
});
expect(wrapper.emitted('setFilters')).toStrictEqual([
[
{
iterationCadenceId: '1349',
iterationId: wildCardId,
},
],
]);
});
expect(wrapper.emitted('setFilters')).toStrictEqual([
[
{
iterationCadenceId: '1349',
iterationId: wildCardId,
},
],
]);
});
});
});

View File

@ -1,7 +1,5 @@
import { GlModal } from '@gitlab/ui';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
import setWindowLocation from 'helpers/set_window_location_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@ -23,8 +21,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
}));
jest.mock('~/boards/eventhub');
Vue.use(Vuex);
const currentBoard = {
id: 'gid://gitlab/Board/1',
name: 'test',
@ -55,14 +51,6 @@ describe('BoardForm', () => {
const findDeleteConfirmation = () => wrapper.findByTestId('delete-confirmation-message');
const findInput = () => wrapper.find('#board-new-name');
const setBoardMock = jest.fn();
const store = new Vuex.Store({
actions: {
setBoard: setBoardMock,
},
});
const defaultHandlers = {
createBoardMutationHandler: jest.fn().mockResolvedValue({
data: {
@ -107,7 +95,6 @@ describe('BoardForm', () => {
isProjectBoard: false,
...provide,
},
store,
attachTo: document.body,
});
};
@ -220,7 +207,7 @@ describe('BoardForm', () => {
});
await waitForPromises();
expect(setBoardMock).toHaveBeenCalledTimes(1);
expect(wrapper.emitted('addBoard')).toHaveLength(1);
});
it('sets error in state if GraphQL mutation fails', async () => {
@ -239,31 +226,8 @@ describe('BoardForm', () => {
expect(requestHandlers.createBoardMutationHandler).toHaveBeenCalled();
await waitForPromises();
expect(setBoardMock).not.toHaveBeenCalled();
expect(cacheUpdates.setError).toHaveBeenCalled();
});
describe('when Apollo boards FF is on', () => {
it('calls a correct GraphQL mutation and emits addBoard event when creating a board', async () => {
createComponent({
props: { canAdminBoard: true, currentPage: formType.new },
provide: { isApolloBoard: true },
});
fillForm();
await waitForPromises();
expect(requestHandlers.createBoardMutationHandler).toHaveBeenCalledWith({
input: expect.objectContaining({
name: 'test',
}),
});
await waitForPromises();
expect(wrapper.emitted('addBoard')).toHaveLength(1);
});
});
});
});
@ -314,8 +278,12 @@ describe('BoardForm', () => {
});
await waitForPromises();
expect(setBoardMock).toHaveBeenCalledTimes(1);
expect(global.window.location.href).not.toContain('?group_by=epic');
expect(eventHub.$emit).toHaveBeenCalledTimes(1);
expect(eventHub.$emit).toHaveBeenCalledWith('updateBoard', {
id: 'gid://gitlab/Board/321',
webPath: 'test-path',
});
});
it('calls GraphQL mutation with correct parameters when issues are grouped by epic', async () => {
@ -335,7 +303,6 @@ describe('BoardForm', () => {
});
await waitForPromises();
expect(setBoardMock).toHaveBeenCalledTimes(1);
expect(global.window.location.href).toContain('?group_by=epic');
});
@ -355,36 +322,8 @@ describe('BoardForm', () => {
expect(requestHandlers.updateBoardMutationHandler).toHaveBeenCalled();
await waitForPromises();
expect(setBoardMock).not.toHaveBeenCalled();
expect(cacheUpdates.setError).toHaveBeenCalled();
});
describe('when Apollo boards FF is on', () => {
it('calls a correct GraphQL mutation and emits updateBoard event when updating a board', async () => {
setWindowLocation('https://test/boards/1');
createComponent({
props: { canAdminBoard: true, currentPage: formType.edit },
provide: { isApolloBoard: true },
});
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
expect(requestHandlers.updateBoardMutationHandler).toHaveBeenCalledWith({
input: expect.objectContaining({
id: currentBoard.id,
}),
});
await waitForPromises();
expect(eventHub.$emit).toHaveBeenCalledTimes(1);
expect(eventHub.$emit).toHaveBeenCalledWith('updateBoard', {
id: 'gid://gitlab/Board/321',
webPath: 'test-path',
});
});
});
});
describe('when deleting a board', () => {
@ -427,7 +366,6 @@ describe('BoardForm', () => {
destroyBoardMutationHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
},
});
jest.spyOn(store, 'dispatch').mockImplementation(() => {});
findModal().vm.$emit('primary');

View File

@ -1,8 +1,6 @@
import { GlButtonGroup } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -18,15 +16,11 @@ import * as cacheUpdates from '~/boards/graphql/cache_updates';
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
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();
const mockClientToggleListCollapsedResolver = jest.fn();
const updateListHandlerSuccess = jest.fn().mockResolvedValue(updateBoardListResponse);
@ -69,10 +63,6 @@ describe('Board List Header Component', () => {
);
}
store = new Vuex.Store({
state: {},
actions: { updateList: updateListSpy, toggleListCollapsed: toggleListCollapsedSpy },
});
fakeApollo = createMockApollo(
[
[listQuery, listQueryHandler],
@ -87,7 +77,6 @@ describe('Board List Header Component', () => {
wrapper = shallowMountExtended(BoardListHeader, {
apolloProvider: fakeApollo,
store,
propsData: {
list: listMock,
filterParams: {},
@ -198,26 +187,34 @@ describe('Board List Header Component', () => {
expect(icon.props('icon')).toBe('chevron-lg-right');
});
it('should dispatch toggleListCollapse when clicking the collapse icon', async () => {
createComponent();
it('set active board item on client when clicking on card', async () => {
createComponent({ listType: ListType.label });
await nextTick();
findCaret().vm.$emit('click');
await nextTick();
expect(toggleListCollapsedSpy).toHaveBeenCalledTimes(1);
expect(mockClientToggleListCollapsedResolver).toHaveBeenCalledWith(
{},
{
list: mockLabelList,
collapsed: true,
},
expect.anything(),
expect.anything(),
);
});
it("when logged in it calls list update and doesn't set localStorage", async () => {
it("when logged in it doesn't set localStorage", async () => {
createComponent({ withLocalStorage: false, currentUserId: 1 });
findCaret().vm.$emit('click');
await nextTick();
expect(updateListSpy).toHaveBeenCalledTimes(1);
expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.collapsed`)).toBe(null);
});
it("when logged out it doesn't call list update and sets localStorage", async () => {
it('when logged out it sets localStorage', async () => {
createComponent({
currentUserId: null,
});
@ -225,7 +222,6 @@ describe('Board List Header Component', () => {
findCaret().vm.$emit('click');
await nextTick();
expect(updateListSpy).not.toHaveBeenCalled();
expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.collapsed`)).toBe(
String(!isCollapsed()),
);
@ -252,86 +248,67 @@ describe('Board List Header Component', () => {
});
});
describe('Apollo boards', () => {
beforeEach(async () => {
createComponent({ listType: ListType.label, injectedProps: { isApolloBoard: true } });
await nextTick();
beforeEach(async () => {
createComponent({ listType: ListType.label });
await nextTick();
});
it('does not call update list mutation when user is not logged in', async () => {
createComponent({ currentUserId: null });
findCaret().vm.$emit('click');
await nextTick();
expect(updateListHandlerSuccess).not.toHaveBeenCalled();
});
it('calls update list mutation when user is logged in', async () => {
createComponent({ currentUserId: 1 });
findCaret().vm.$emit('click');
await nextTick();
expect(updateListHandlerSuccess).toHaveBeenCalledWith({
listId: mockLabelList.id,
collapsed: true,
});
});
describe('when fetch list query fails', () => {
const errorMessage = 'Failed to fetch list';
const listQueryHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage));
beforeEach(() => {
createComponent({
listQueryHandler: listQueryHandlerFailure,
});
});
it('set active board item on client when clicking on card', async () => {
findCaret().vm.$emit('click');
await nextTick();
it('sets error', async () => {
await waitForPromises();
expect(mockClientToggleListCollapsedResolver).toHaveBeenCalledWith(
{},
{
list: mockLabelList,
collapsed: true,
},
expect.anything(),
expect.anything(),
);
expect(cacheUpdates.setError).toHaveBeenCalled();
});
});
describe('when update list mutation fails', () => {
const errorMessage = 'Failed to update list';
const updateListHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage));
beforeEach(() => {
createComponent({
currentUserId: 1,
updateListHandler: updateListHandlerFailure,
});
});
it('does not call update list mutation when user is not logged in', async () => {
createComponent({ currentUserId: null, injectedProps: { isApolloBoard: true } });
it('sets error', async () => {
await waitForPromises();
findCaret().vm.$emit('click');
await nextTick();
await waitForPromises();
expect(updateListHandlerSuccess).not.toHaveBeenCalled();
});
it('calls update list mutation when user is logged in', async () => {
createComponent({ currentUserId: 1, injectedProps: { isApolloBoard: true } });
findCaret().vm.$emit('click');
await nextTick();
expect(updateListHandlerSuccess).toHaveBeenCalledWith({
listId: mockLabelList.id,
collapsed: true,
});
});
describe('when fetch list query fails', () => {
const errorMessage = 'Failed to fetch list';
const listQueryHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage));
beforeEach(() => {
createComponent({
listQueryHandler: listQueryHandlerFailure,
injectedProps: { isApolloBoard: true },
});
});
it('sets error', async () => {
await waitForPromises();
expect(cacheUpdates.setError).toHaveBeenCalled();
});
});
describe('when update list mutation fails', () => {
const errorMessage = 'Failed to update list';
const updateListHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage));
beforeEach(() => {
createComponent({
currentUserId: 1,
updateListHandler: updateListHandlerFailure,
injectedProps: { isApolloBoard: true },
});
});
it('sets error', async () => {
await waitForPromises();
findCaret().vm.$emit('click');
await waitForPromises();
expect(cacheUpdates.setError).toHaveBeenCalled();
});
expect(cacheUpdates.setError).toHaveBeenCalled();
});
});
});

View File

@ -1,7 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import BoardNewIssue from '~/boards/components/board_new_issue.vue';
@ -15,18 +13,12 @@ import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import {
mockList,
mockGroupProjects,
mockIssue,
mockIssue2,
mockProjectBoardResponse,
mockGroupBoardResponse,
} from '../mock_data';
Vue.use(Vuex);
Vue.use(VueApollo);
const addListNewIssuesSpy = jest.fn().mockResolvedValue();
const mockActions = { addListNewIssue: addListNewIssuesSpy };
const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
@ -36,20 +28,12 @@ const mockApollo = createMockApollo([
]);
const createComponent = ({
state = {},
actions = mockActions,
getters = { getBoardItemsByList: () => () => [] },
isGroupBoard = true,
data = { selectedProject: mockGroupProjects[0] },
provide = {},
} = {}) =>
shallowMount(BoardNewIssue, {
apolloProvider: mockApollo,
store: new Vuex.Store({
state,
actions,
getters,
}),
propsData: {
list: mockList,
boardId: 'gid://gitlab/Board/1',
@ -63,7 +47,6 @@ const createComponent = ({
isGroupBoard,
boardType: 'group',
isEpicBoard: false,
isApolloBoard: false,
...provide,
},
stubs: {
@ -82,6 +65,32 @@ describe('Issue boards new issue form', () => {
await nextTick();
});
it.each`
boardType | queryHandler | notCalledHandler
${WORKSPACE_GROUP} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
${WORKSPACE_PROJECT} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
`(
'fetches $boardType board and emits addNewIssue event',
async ({ boardType, queryHandler, notCalledHandler }) => {
wrapper = createComponent({
provide: {
boardType,
isProjectBoard: boardType === WORKSPACE_PROJECT,
isGroupBoard: boardType === WORKSPACE_GROUP,
},
});
await nextTick();
findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' });
await nextTick();
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
expect(wrapper.emitted('addNewIssue')[0][0]).toMatchObject({ title: 'Foo' });
},
);
it('renders board-new-item component', () => {
const boardNewItem = findBoardNewItem();
expect(boardNewItem.exists()).toBe(true);
@ -93,51 +102,6 @@ describe('Issue boards new issue form', () => {
});
});
it('calls addListNewIssue action when `board-new-item` emits form-submit event', async () => {
findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' });
await nextTick();
expect(addListNewIssuesSpy).toHaveBeenCalledWith(expect.any(Object), {
list: mockList,
issueInput: {
title: 'Foo',
labelIds: [],
assigneeIds: [],
milestoneId: undefined,
projectPath: mockGroupProjects[0].fullPath,
moveAfterId: undefined,
},
});
});
describe('when list has an existing issues', () => {
beforeEach(() => {
wrapper = createComponent({
getters: {
getBoardItemsByList: () => () => [mockIssue, mockIssue2],
},
isGroupBoard: true,
});
});
it('uses the first issue ID as moveAfterId', async () => {
findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' });
await nextTick();
expect(addListNewIssuesSpy).toHaveBeenCalledWith(expect.any(Object), {
list: mockList,
issueInput: {
title: 'Foo',
labelIds: [],
assigneeIds: [],
milestoneId: undefined,
projectPath: mockGroupProjects[0].fullPath,
moveAfterId: mockIssue.id,
},
});
});
});
it('emits event `toggle-issue-form` with current list Id suffix on eventHub when `board-new-item` emits form-cancel event', async () => {
jest.spyOn(eventHub, '$emit').mockImplementation();
findBoardNewItem().vm.$emit('form-cancel');
@ -168,33 +132,4 @@ describe('Issue boards new issue form', () => {
expect(projectSelect.exists()).toBe(false);
});
});
describe('Apollo boards', () => {
it.each`
boardType | queryHandler | notCalledHandler
${WORKSPACE_GROUP} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
${WORKSPACE_PROJECT} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
`(
'fetches $boardType board and emits addNewIssue event',
async ({ boardType, queryHandler, notCalledHandler }) => {
wrapper = createComponent({
provide: {
boardType,
isProjectBoard: boardType === WORKSPACE_PROJECT,
isGroupBoard: boardType === WORKSPACE_GROUP,
isApolloBoard: true,
},
});
await nextTick();
findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' });
await nextTick();
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
expect(wrapper.emitted('addNewIssue')[0][0]).toMatchObject({ title: 'Foo' });
},
);
});
});

View File

@ -1,8 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -21,18 +19,11 @@ import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
import { mockProjectBoardResponse, mockGroupBoardResponse } from '../mock_data';
Vue.use(VueApollo);
Vue.use(Vuex);
describe('BoardTopBar', () => {
let wrapper;
let mockApollo;
const createStore = () => {
return new Vuex.Store({
state: {},
});
};
const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
const errorMessage = 'Failed to fetch board';
@ -43,14 +34,12 @@ describe('BoardTopBar', () => {
projectBoardQueryHandler = projectBoardQueryHandlerSuccess,
groupBoardQueryHandler = groupBoardQueryHandlerSuccess,
} = {}) => {
const store = createStore();
mockApollo = createMockApollo([
[projectBoardQuery, projectBoardQueryHandler],
[groupBoardQuery, groupBoardQueryHandler],
]);
wrapper = shallowMount(BoardTopBar, {
store,
apolloProvider: mockApollo,
propsData: {
boardId: 'gid://gitlab/Board/1',
@ -67,7 +56,7 @@ describe('BoardTopBar', () => {
isIssueBoard: true,
isEpicBoard: false,
isGroupBoard: true,
isApolloBoard: false,
// isApolloBoard: false,
...provide,
},
stubs: { IssueBoardFilteredSearch },
@ -127,45 +116,41 @@ describe('BoardTopBar', () => {
});
});
describe('Apollo boards', () => {
it.each`
boardType | queryHandler | notCalledHandler
${WORKSPACE_GROUP} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
${WORKSPACE_PROJECT} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
`('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => {
createComponent({
provide: {
boardType,
isProjectBoard: boardType === WORKSPACE_PROJECT,
isGroupBoard: boardType === WORKSPACE_GROUP,
isApolloBoard: true,
},
});
await nextTick();
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
it.each`
boardType | queryHandler | notCalledHandler
${WORKSPACE_GROUP} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
${WORKSPACE_PROJECT} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
`('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => {
createComponent({
provide: {
boardType,
isProjectBoard: boardType === WORKSPACE_PROJECT,
isGroupBoard: boardType === WORKSPACE_GROUP,
},
});
it.each`
boardType
${WORKSPACE_GROUP}
${WORKSPACE_PROJECT}
`('sets error when $boardType board query fails', async ({ boardType }) => {
createComponent({
provide: {
boardType,
isProjectBoard: boardType === WORKSPACE_PROJECT,
isGroupBoard: boardType === WORKSPACE_GROUP,
isApolloBoard: true,
},
groupBoardQueryHandler: boardQueryHandlerFailure,
projectBoardQueryHandler: boardQueryHandlerFailure,
});
await nextTick();
await waitForPromises();
expect(cacheUpdates.setError).toHaveBeenCalled();
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
});
it.each`
boardType
${WORKSPACE_GROUP}
${WORKSPACE_PROJECT}
`('sets error when $boardType board query fails', async ({ boardType }) => {
createComponent({
provide: {
boardType,
isProjectBoard: boardType === WORKSPACE_PROJECT,
isGroupBoard: boardType === WORKSPACE_GROUP,
},
groupBoardQueryHandler: boardQueryHandlerFailure,
projectBoardQueryHandler: boardQueryHandlerFailure,
});
await waitForPromises();
expect(cacheUpdates.setError).toHaveBeenCalled();
});
});

View File

@ -6,7 +6,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { createStore } from '~/boards/stores';
import issueSetTitleMutation from '~/boards/graphql/issue_set_title.mutation.graphql';
import * as cacheUpdates from '~/boards/graphql/cache_updates';
import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql';
@ -32,11 +31,10 @@ const TEST_ISSUE_B = {
describe('BoardSidebarTitle', () => {
let wrapper;
let store;
let storeDispatch;
let mockApollo;
const issueSetTitleMutationHandlerSuccess = jest.fn().mockResolvedValue(updateIssueTitleResponse);
const issueSetTitleMutationHandlerFailure = jest.fn().mockRejectedValue(new Error('error'));
const updateEpicTitleMutationHandlerSuccess = jest
.fn()
.mockResolvedValue(updateEpicTitleResponse);
@ -47,28 +45,25 @@ describe('BoardSidebarTitle', () => {
afterEach(() => {
localStorage.clear();
store = null;
});
const createWrapper = ({ item = TEST_ISSUE_A, provide = {} } = {}) => {
store = createStore();
store.state.boardItems = { [item.id]: { ...item } };
store.dispatch('setActiveId', { id: item.id });
const createWrapper = ({
item = TEST_ISSUE_A,
provide = {},
issueSetTitleMutationHandler = issueSetTitleMutationHandlerSuccess,
} = {}) => {
mockApollo = createMockApollo([
[issueSetTitleMutation, issueSetTitleMutationHandlerSuccess],
[issueSetTitleMutation, issueSetTitleMutationHandler],
[updateEpicTitleMutation, updateEpicTitleMutationHandlerSuccess],
]);
storeDispatch = jest.spyOn(store, 'dispatch');
wrapper = shallowMountExtended(BoardSidebarTitle, {
store,
apolloProvider: mockApollo,
provide: {
canUpdate: true,
fullPath: 'gitlab-org',
issuableType: 'issue',
isEpicBoard: false,
isApolloBoard: false,
...provide,
},
propsData: {
@ -122,13 +117,6 @@ describe('BoardSidebarTitle', () => {
expect(findCollapsed().isVisible()).toBe(true);
});
it('commits change to the server', () => {
expect(storeDispatch).toHaveBeenCalledWith('setActiveItemTitle', {
projectPath: 'h/b',
title: 'New item title',
});
});
it('renders correct title', async () => {
createWrapper({ item: { ...TEST_ISSUE_A, title: TEST_TITLE } });
await waitForPromises();
@ -137,6 +125,31 @@ describe('BoardSidebarTitle', () => {
});
});
it.each`
issuableType | isEpicBoard | queryHandler | notCalledHandler
${'issue'} | ${false} | ${issueSetTitleMutationHandlerSuccess} | ${updateEpicTitleMutationHandlerSuccess}
${'epic'} | ${true} | ${updateEpicTitleMutationHandlerSuccess} | ${issueSetTitleMutationHandlerSuccess}
`(
'updates $issuableType title',
async ({ issuableType, isEpicBoard, queryHandler, notCalledHandler }) => {
createWrapper({
provide: {
issuableType,
isEpicBoard,
},
});
await nextTick();
findFormInput().vm.$emit('input', TEST_TITLE);
findForm().vm.$emit('submit', { preventDefault: () => {} });
await nextTick();
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
},
);
describe('when submitting and invalid title', () => {
beforeEach(async () => {
createWrapper();
@ -146,8 +159,8 @@ describe('BoardSidebarTitle', () => {
await nextTick();
});
it('commits change to the server', () => {
expect(storeDispatch).not.toHaveBeenCalled();
it('does not update title', () => {
expect(issueSetTitleMutationHandlerSuccess).not.toHaveBeenCalled();
});
});
@ -194,7 +207,7 @@ describe('BoardSidebarTitle', () => {
});
it('collapses sidebar and render former title', () => {
expect(storeDispatch).not.toHaveBeenCalled();
expect(issueSetTitleMutationHandlerSuccess).not.toHaveBeenCalled();
expect(findCollapsed().isVisible()).toBe(true);
expect(findTitle().text()).toBe(TEST_ISSUE_B.title);
});
@ -202,47 +215,23 @@ describe('BoardSidebarTitle', () => {
describe('when the mutation fails', () => {
beforeEach(async () => {
createWrapper({ item: TEST_ISSUE_B });
createWrapper({
item: TEST_ISSUE_B,
issueSetTitleMutationHandler: issueSetTitleMutationHandlerFailure,
});
findFormInput().vm.$emit('input', 'Invalid title');
findForm().vm.$emit('submit', { preventDefault: () => {} });
await nextTick();
});
it('collapses sidebar and renders former item title', () => {
it('collapses sidebar and renders former item title', async () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findTitle().text()).toContain(TEST_ISSUE_B.title);
await waitForPromises();
expect(cacheUpdates.setError).toHaveBeenCalledWith(
expect.objectContaining({ message: 'An error occurred when updating the title' }),
);
});
});
describe('Apollo boards', () => {
it.each`
issuableType | isEpicBoard | queryHandler | notCalledHandler
${'issue'} | ${false} | ${issueSetTitleMutationHandlerSuccess} | ${updateEpicTitleMutationHandlerSuccess}
${'epic'} | ${true} | ${updateEpicTitleMutationHandlerSuccess} | ${issueSetTitleMutationHandlerSuccess}
`(
'updates $issuableType title',
async ({ issuableType, isEpicBoard, queryHandler, notCalledHandler }) => {
createWrapper({
provide: {
issuableType,
isEpicBoard,
isApolloBoard: true,
},
});
await nextTick();
findFormInput().vm.$emit('input', TEST_TITLE);
findForm().vm.$emit('submit', { preventDefault: () => {} });
await nextTick();
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
},
);
});
});

View File

@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlModal } from '@gitlab/ui';
import { GlModal } from '@gitlab/ui';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import CommitSection from '~/ci/pipeline_editor/components/commit/commit_section.vue';
@ -60,7 +61,7 @@ describe('Pipeline editor home wrapper', () => {
const findPipelineEditorFileTree = () => wrapper.findComponent(PipelineEditorFileTree);
const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader);
const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs);
const findFileTreeBtn = () => wrapper.findByTestId('file-tree-toggle');
const findPipelineEditorFileNav = () => wrapper.findComponent(PipelineEditorFileNav);
const clickHelpBtn = async () => {
await findPipelineEditorDrawer().vm.$emit('switch-drawer', EDITOR_APP_DRAWER_HELP);
@ -279,24 +280,16 @@ describe('Pipeline editor home wrapper', () => {
describe('file tree', () => {
const toggleFileTree = async () => {
await findFileTreeBtn().vm.$emit('click');
findPipelineEditorFileNav().vm.$emit('toggle-file-tree');
await nextTick();
};
describe('button toggle', () => {
describe('file navigation', () => {
beforeEach(() => {
createComponent({
stubs: {
GlButton,
PipelineEditorFileNav,
},
});
createComponent({});
});
it('shows button toggle', () => {
expect(findFileTreeBtn().exists()).toBe(true);
});
it('toggles the drawer on button click', async () => {
it('toggles the drawer on `toggle-file-tree` event', async () => {
await toggleFileTree();
expect(findPipelineEditorFileTree().exists()).toBe(true);

View File

@ -1,4 +1,4 @@
import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlFilteredSearch, GlSorting } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { assertProps } from 'helpers/assert_props';
@ -32,7 +32,12 @@ describe('RunnerList', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
const findSortOptions = () => wrapper.findAllComponents(GlDropdownItem);
const findGlSorting = () => wrapper.findComponent(GlSorting);
const getSortOptions = () => findGlSorting().props('sortOptions');
const getSelectedSortOption = () => {
const sortBy = findGlSorting().props('sortBy');
return getSortOptions().find(({ value }) => sortBy === value)?.text;
};
const mockOtherSort = CONTACTED_DESC;
const mockFilters = [
@ -56,8 +61,6 @@ describe('RunnerList', () => {
stubs: {
FilteredSearch,
GlFilteredSearch,
GlDropdown,
GlDropdownItem,
},
...options,
});
@ -74,9 +77,10 @@ describe('RunnerList', () => {
it('sets sorting options', () => {
const SORT_OPTIONS_COUNT = 2;
expect(findSortOptions()).toHaveLength(SORT_OPTIONS_COUNT);
expect(findSortOptions().at(0).text()).toBe('Created date');
expect(findSortOptions().at(1).text()).toBe('Last contact');
const sortOptionsProp = getSortOptions();
expect(sortOptionsProp).toHaveLength(SORT_OPTIONS_COUNT);
expect(sortOptionsProp[0].text).toBe('Created date');
expect(sortOptionsProp[1].text).toBe('Last contact');
});
it('sets tokens to the filtered search', () => {
@ -141,12 +145,7 @@ describe('RunnerList', () => {
});
it('sort option is selected', () => {
expect(
findSortOptions()
.filter((w) => w.props('isChecked'))
.at(0)
.text(),
).toEqual('Last contact');
expect(getSelectedSortOption()).toBe('Last contact');
});
it('when the user sets a filter, the "search" preserves the other filters', async () => {
@ -181,7 +180,7 @@ describe('RunnerList', () => {
});
it('when the user sets a sorting method, the "search" is emitted with the sort', () => {
findSortOptions().at(1).vm.$emit('click');
findGlSorting().vm.$emit('sortByChange', 2);
expectToHaveLastEmittedInput({
runnerType: null,

View File

@ -71,11 +71,8 @@ describe('RepoCommitSection', () => {
createComponent();
});
it('renders no changes text', () => {
expect(wrapper.findComponent(EmptyState).text().trim()).toContain('No changes');
expect(wrapper.findComponent(EmptyState).find('img').attributes('src')).toBe(
TEST_NO_CHANGES_SVG,
);
it('renders empty state component', () => {
expect(wrapper.findComponent(EmptyState).exists()).toBe(true);
});
});

View File

@ -1,11 +1,15 @@
import { GlFormCheckbox } from '@gitlab/ui';
import { nextTick } from 'vue';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
Vue.use(Vuex);
describe('JiraTriggerFields', () => {
let wrapper;
let store;
const defaultProps = {
initialTriggerCommit: false,
@ -14,12 +18,16 @@ describe('JiraTriggerFields', () => {
};
const createComponent = (props, isInheriting = false) => {
wrapper = mountExtended(JiraTriggerFields, {
propsData: { ...defaultProps, ...props },
computed: {
store = new Vuex.Store({
getters: {
isInheriting: () => isInheriting,
},
});
wrapper = mountExtended(JiraTriggerFields, {
propsData: { ...defaultProps, ...props },
store,
});
};
const findCommentSettings = () => wrapper.findByTestId('comment-settings');

View File

@ -1,12 +1,17 @@
import { nextTick } from 'vue';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import TriggerField from '~/integrations/edit/components/trigger_field.vue';
import { integrationTriggerEventTitles } from '~/integrations/constants';
Vue.use(Vuex);
describe('TriggerField', () => {
let wrapper;
let store;
const defaultProps = {
event: { name: 'push_events' },
@ -15,12 +20,16 @@ describe('TriggerField', () => {
const mockField = { name: 'push_channel' };
const createComponent = ({ props = {}, isInheriting = false } = {}) => {
wrapper = shallowMount(TriggerField, {
propsData: { ...defaultProps, ...props },
computed: {
store = new Vuex.Store({
getters: {
isInheriting: () => isInheriting,
},
});
wrapper = shallowMount(TriggerField, {
propsData: { ...defaultProps, ...props },
store,
});
};
const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox);

View File

@ -1,23 +1,32 @@
import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { placeholderForType } from 'jh_else_ce/integrations/constants';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
Vue.use(Vuex);
describe('TriggerFields', () => {
let wrapper;
let store;
const defaultProps = {
type: 'slack',
};
const createComponent = (props, isInheriting = false) => {
wrapper = mountExtended(TriggerFields, {
propsData: { ...defaultProps, ...props },
computed: {
store = new Vuex.Store({
getters: {
isInheriting: () => isInheriting,
},
});
wrapper = mountExtended(TriggerFields, {
propsData: { ...defaultProps, ...props },
store,
});
};
const findTriggerLabel = () => wrapper.findByTestId('trigger-fields-group').find('label');

View File

@ -16,6 +16,7 @@ import {
NAMESPACE_STORAGE_TYPES,
TOTAL_USAGE_DEFAULT_TEXT,
} from '~/usage_quotas/storage/constants';
import getCostFactoredProjectStorageStatistics from 'ee_else_ce/usage_quotas/storage/queries/cost_factored_project_storage.query.graphql';
import getProjectStorageStatistics from 'ee_else_ce/usage_quotas/storage/queries/project_storage.query.graphql';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import {
@ -38,7 +39,10 @@ describe('ProjectStorageApp', () => {
response = jest.fn().mockResolvedValue(mockedValue);
}
const requestHandlers = [[getProjectStorageStatistics, response]];
const requestHandlers = [
[getProjectStorageStatistics, response],
[getCostFactoredProjectStorageStatistics, response],
];
return createMockApollo(requestHandlers);
};
@ -187,4 +191,30 @@ describe('ProjectStorageApp', () => {
]);
});
});
describe('when displayCostFactoredStorageSizeOnProjectPages feature flag is enabled', () => {
let mockApollo;
beforeEach(async () => {
mockApollo = createMockApolloProvider({
mockedValue: mockGetProjectStorageStatisticsGraphQLResponse,
});
createComponent({
mockApollo,
provide: {
glFeatures: {
displayCostFactoredStorageSizeOnProjectPages: true,
},
},
});
await waitForPromises();
});
it('renders correct total usage', () => {
const expectedValue = numberToHumanSize(
mockGetProjectStorageStatisticsGraphQLResponse.data.project.statistics.storageSize,
1,
);
expect(findUsagePercentage().text()).toBe(expectedValue);
});
});
});

View File

@ -1,11 +1,4 @@
import {
GlFilteredSearch,
GlButtonGroup,
GlButton,
GlDropdown,
GlDropdownItem,
GlFormCheckbox,
} from '@gitlab/ui';
import { GlDropdownItem, GlSorting, GlFilteredSearch, GlFormCheckbox } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
@ -13,7 +6,6 @@ import RecentSearchesService from '~/filtered_search/services/recent_searches_se
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
import {
FILTERED_SEARCH_TERM,
SORT_DIRECTION,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
@ -48,6 +40,7 @@ const createComponent = ({
recentSearchesStorageKey = 'requirements',
tokens = mockAvailableTokens,
sortOptions,
initialSortBy,
initialFilterValue = [],
showCheckbox = false,
checkboxChecked = false,
@ -61,6 +54,7 @@ const createComponent = ({
recentSearchesStorageKey,
tokens,
sortOptions,
initialSortBy,
initialFilterValue,
showCheckbox,
checkboxChecked,
@ -72,34 +66,38 @@ const createComponent = ({
describe('FilteredSearchBarRoot', () => {
let wrapper;
const findGlButton = () => wrapper.findComponent(GlButton);
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
const findGlSorting = () => wrapper.findComponent(GlSorting);
const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
beforeEach(() => {
wrapper = createComponent({ sortOptions: mockSortOptions });
});
describe('data', () => {
it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props and displays the sort dropdown', () => {
expect(wrapper.vm.filterValue).toEqual([]);
expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0]);
expect(wrapper.vm.selectedSortDirection).toBe(SORT_DIRECTION.descending);
expect(wrapper.findComponent(GlButtonGroup).exists()).toBe(true);
expect(wrapper.findComponent(GlButton).exists()).toBe(true);
expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true);
describe('when `sortOptions` are provided', () => {
beforeEach(() => {
wrapper = createComponent({ sortOptions: mockSortOptions });
});
it('sets a correct initial value for GlFilteredSearch', () => {
expect(findGlFilteredSearch().props('value')).toEqual([]);
});
it('emits an event with the selectedSortOption provided by default', async () => {
findGlSorting().vm.$emit('sortByChange', mockSortOptions[1].id);
await nextTick();
expect(wrapper.emitted('onSort')[0]).toEqual([mockSortOptions[1].sortDirection.descending]);
});
it('emits an event with the selectedSortDirection provided by default', async () => {
findGlSorting().vm.$emit('sortDirectionChange', true);
await nextTick();
expect(wrapper.emitted('onSort')[0]).toEqual([mockSortOptions[0].sortDirection.ascending]);
});
});
it('does not initialize `selectedSortOption` and `selectedSortDirection` when `sortOptions` is not applied and hides the sort dropdown', () => {
const wrapperNoSort = createComponent();
it('does not initialize the sort dropdown when `sortOptions` are not provided', () => {
wrapper = createComponent();
expect(wrapperNoSort.vm.filterValue).toEqual([]);
expect(wrapperNoSort.vm.selectedSortOption).toBe(undefined);
expect(wrapperNoSort.findComponent(GlButtonGroup).exists()).toBe(false);
expect(wrapperNoSort.findComponent(GlButton).exists()).toBe(false);
expect(wrapperNoSort.findComponent(GlDropdown).exists()).toBe(false);
expect(wrapperNoSort.findComponent(GlDropdownItem).exists()).toBe(false);
expect(findGlSorting().exists()).toBe(false);
});
});
@ -125,27 +123,27 @@ describe('FilteredSearchBarRoot', () => {
});
describe('sortDirectionIcon', () => {
it('renders `sort-highest` descending icon by default', () => {
expect(findGlButton().props('icon')).toBe('sort-highest');
expect(findGlButton().attributes()).toMatchObject({
'aria-label': 'Sort direction: Descending',
title: 'Sort direction: Descending',
});
beforeEach(() => {
wrapper = createComponent({ sortOptions: mockSortOptions });
});
it('passes isAscending=false to GlSorting by default', () => {
expect(findGlSorting().props('isAscending')).toBe(false);
});
it('renders `sort-lowest` ascending icon when the sort button is clicked', async () => {
findGlButton().vm.$emit('click');
findGlSorting().vm.$emit('sortDirectionChange', true);
await nextTick();
expect(findGlButton().props('icon')).toBe('sort-lowest');
expect(findGlButton().attributes()).toMatchObject({
'aria-label': 'Sort direction: Ascending',
title: 'Sort direction: Ascending',
});
expect(findGlSorting().props('isAscending')).toBe(true);
});
});
describe('filteredRecentSearches', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('returns array of recent searches filtering out any string type (unsupported) items', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
@ -227,34 +225,37 @@ describe('FilteredSearchBarRoot', () => {
});
});
describe('handleSortOptionClick', () => {
it('emits component event `onSort` with selected sort by value', () => {
wrapper.vm.handleSortOptionClick(mockSortOptions[1]);
describe('handleSortOptionChange', () => {
it('emits component event `onSort` with selected sort by value', async () => {
wrapper = createComponent({ sortOptions: mockSortOptions });
findGlSorting().vm.$emit('sortByChange', mockSortOptions[1].id);
await nextTick();
expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[1]);
expect(wrapper.emitted('onSort')[0]).toEqual([mockSortOptions[1].sortDirection.descending]);
});
});
describe('handleSortDirectionClick', () => {
describe('handleSortDirectionChange', () => {
beforeEach(() => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortOption: mockSortOptions[0],
wrapper = createComponent({
sortOptions: mockSortOptions,
initialSortBy: mockSortOptions[0].sortDirection.descending,
});
});
it('sets `selectedSortDirection` to be opposite of its current value', () => {
expect(wrapper.vm.selectedSortDirection).toBe(SORT_DIRECTION.descending);
it('sets sort direction to be opposite of its current value', async () => {
expect(findGlSorting().props('isAscending')).toBe(false);
wrapper.vm.handleSortDirectionClick();
findGlSorting().vm.$emit('sortDirectionChange', true);
await nextTick();
expect(wrapper.vm.selectedSortDirection).toBe(SORT_DIRECTION.ascending);
expect(findGlSorting().props('isAscending')).toBe(true);
});
it('emits component event `onSort` with opposite of currently selected sort by value', () => {
wrapper.vm.handleSortDirectionClick();
findGlSorting().vm.$emit('sortDirectionChange', true);
expect(wrapper.emitted('onSort')[0]).toEqual([mockSortOptions[0].sortDirection.ascending]);
});
@ -288,6 +289,8 @@ describe('FilteredSearchBarRoot', () => {
const mockFilters = [tokenValueAuthor, 'foo'];
beforeEach(async () => {
wrapper = createComponent();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
@ -358,19 +361,14 @@ describe('FilteredSearchBarRoot', () => {
});
describe('template', () => {
beforeEach(async () => {
it('renders gl-filtered-search component', async () => {
wrapper = createComponent();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortOption: mockSortOptions[0],
selectedSortDirection: SORT_DIRECTION.descending,
await wrapper.setData({
recentSearches: mockHistoryItems,
});
await nextTick();
});
it('renders gl-filtered-search component', () => {
const glFilteredSearchEl = wrapper.findComponent(GlFilteredSearch);
expect(glFilteredSearchEl.props('placeholder')).toBe('Filter requirements');
@ -454,25 +452,28 @@ describe('FilteredSearchBarRoot', () => {
});
it('renders sort dropdown component', () => {
expect(wrapper.findComponent(GlButtonGroup).exists()).toBe(true);
expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
expect(wrapper.findComponent(GlDropdown).props('text')).toBe(mockSortOptions[0].title);
wrapper = createComponent({ sortOptions: mockSortOptions });
expect(findGlSorting().exists()).toBe(true);
});
it('renders sort dropdown items', () => {
const dropdownItemsEl = wrapper.findAllComponents(GlDropdownItem);
wrapper = createComponent({ sortOptions: mockSortOptions });
expect(dropdownItemsEl).toHaveLength(mockSortOptions.length);
expect(dropdownItemsEl.at(0).text()).toBe(mockSortOptions[0].title);
expect(dropdownItemsEl.at(0).props('isChecked')).toBe(true);
expect(dropdownItemsEl.at(1).text()).toBe(mockSortOptions[1].title);
});
const { sortOptions, sortBy } = findGlSorting().props();
it('renders sort direction button', () => {
const sortButtonEl = wrapper.findComponent(GlButton);
expect(sortOptions).toEqual([
{
value: mockSortOptions[0].id,
text: mockSortOptions[0].title,
},
{
value: mockSortOptions[1].id,
text: mockSortOptions[1].title,
},
]);
expect(sortButtonEl.attributes('title')).toBe('Sort direction: Descending');
expect(sortButtonEl.props('icon')).toBe('sort-highest');
expect(sortBy).toBe(mockSortOptions[0].id);
});
});
@ -483,6 +484,10 @@ describe('FilteredSearchBarRoot', () => {
value: { data: '' },
};
beforeEach(() => {
wrapper = createComponent({ sortOptions: mockSortOptions });
});
it('syncs filter value', async () => {
await wrapper.setProps({ initialFilterValue: [tokenValue], syncFilterAndSort: true });
@ -498,17 +503,33 @@ describe('FilteredSearchBarRoot', () => {
it('syncs sort values', async () => {
await wrapper.setProps({ initialSortBy: 'updated_asc', syncFilterAndSort: true });
expect(findGlDropdown().props('text')).toBe('Last updated');
expect(findGlButton().props('icon')).toBe('sort-lowest');
expect(findGlButton().attributes('aria-label')).toBe('Sort direction: Ascending');
expect(findGlSorting().props()).toMatchObject({
sortBy: 2,
isAscending: true,
});
});
it('does not sync sort values when syncFilterAndSort=false', async () => {
await wrapper.setProps({ initialSortBy: 'updated_asc', syncFilterAndSort: false });
expect(findGlDropdown().props('text')).toBe('Created date');
expect(findGlButton().props('icon')).toBe('sort-highest');
expect(findGlButton().attributes('aria-label')).toBe('Sort direction: Descending');
expect(findGlSorting().props()).toMatchObject({
sortBy: 1,
isAscending: false,
});
});
it('does not sync sort values when initialSortBy is unset', async () => {
// Give initialSort some value which changes the current sort option...
await wrapper.setProps({ initialSortBy: 'updated_asc', syncFilterAndSort: true });
// ... Read the new sort options...
const { sortBy, isAscending } = findGlSorting().props();
// ... Then *unset* initialSortBy...
await wrapper.setProps({ initialSortBy: undefined });
// ... The sort options should not have changed.
expect(findGlSorting().props()).toMatchObject({ sortBy, isAscending });
});
});
});

View File

@ -1600,18 +1600,21 @@ export const availableWorkItemsResponse = {
id: 'gid://gitlab/WorkItem/458',
iid: '2',
title: 'Task 1',
confidential: false,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/459',
iid: '3',
title: 'Task 2',
confidential: false,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/460',
iid: '4',
title: 'Task 3',
confidential: false,
__typename: 'WorkItem',
},
],
@ -1631,18 +1634,21 @@ export const availableObjectivesResponse = {
id: 'gid://gitlab/WorkItem/716',
iid: '122',
title: 'Objective 101',
confidential: false,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/712',
iid: '118',
title: 'Objective 103',
confidential: false,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/711',
iid: '117',
title: 'Objective 102',
confidential: false,
__typename: 'WorkItem',
},
],
@ -1662,6 +1668,7 @@ export const searchedObjectiveResponse = {
id: 'gid://gitlab/WorkItem/716',
iid: '122',
title: 'Objective 101',
confidential: false,
__typename: 'WorkItem',
},
],
@ -1681,6 +1688,7 @@ export const searchWorkItemsTextResponse = {
id: 'gid://gitlab/WorkItem/459',
iid: '3',
title: 'Task 2',
confidential: false,
__typename: 'WorkItem',
},
],
@ -1703,6 +1711,7 @@ export const searchWorkItemsIidResponse = {
id: 'gid://gitlab/WorkItem/460',
iid: '101',
title: 'Task 3',
confidential: false,
__typename: 'WorkItem',
},
],
@ -1722,6 +1731,7 @@ export const searchWorkItemsTextIidResponse = {
id: 'gid://gitlab/WorkItem/459',
iid: '3',
title: 'Task 123',
confidential: false,
__typename: 'WorkItem',
},
],
@ -1732,6 +1742,7 @@ export const searchWorkItemsTextIidResponse = {
id: 'gid://gitlab/WorkItem/460',
iid: '123',
title: 'Task 2',
confidential: false,
__typename: 'WorkItem',
},
],

View File

@ -27,7 +27,6 @@ RSpec.describe 'cross-database foreign keys' do
'namespace_commit_emails.email_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/429804
'namespace_commit_emails.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/429804
'path_locks.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/429380
'project_authorizations.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422044
'protected_branch_push_access_levels.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/431054
'protected_branch_merge_access_levels.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/431055
'security_orchestration_policy_configurations.bot_user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/429438

Some files were not shown because too many files have changed in this diff Show More