Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3bba41a8c5
commit
d0713b8075
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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 === '!=');
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 }) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
linkedPipelines: null,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
linkedPipelines: {
|
||||
query: getLinkedPipelinesQuery,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ query groupWorkItems(
|
|||
id
|
||||
iid
|
||||
title
|
||||
confidential
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ module ExternalRedirect
|
|||
redirect_to url_param
|
||||
else
|
||||
render layout: 'fullscreen', locals: {
|
||||
minimal: true,
|
||||
url: url_param
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
a567da73e9ecdf930ad89c68fba02e8b30aba9e8e460a00e0bf272067ca21409
|
||||
|
|
@ -0,0 +1 @@
|
|||
187bf045979bb377e9999a260791075cab983eeda34db7ca3851720d6c5f79f9
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -137,7 +137,7 @@ Pipelines for forks display with the **fork** badge in the parent project:
|
|||
|
||||

|
||||
|
||||
### 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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -1273,9 +1273,6 @@ msgstr ""
|
|||
msgid "%{time} UTC"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{title} changes"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{totalCpu} (%{freeSpacePercentage}%{percentSymbol} free)"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ global:
|
|||
image:
|
||||
pullPolicy: Always
|
||||
ingress:
|
||||
annotations:
|
||||
external-dns.alpha.kubernetes.io/ttl: 10
|
||||
configureCertmanager: false
|
||||
tls:
|
||||
secretName: review-apps-tls
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 } });
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue