Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-05-09 09:13:16 +00:00
parent e168d3919a
commit ece36a2169
35 changed files with 308 additions and 257 deletions

View File

@ -1 +1 @@
v16.0.0-rc2
v16.0.0

View File

@ -130,8 +130,11 @@ export default {
},
},
watch: {
message() {
this.renderPreview();
message: {
handler() {
this.renderPreview();
},
immediate: true,
},
},
methods: {

View File

@ -1,24 +1,42 @@
<script>
import { mapGetters } from 'vuex';
import { refreshCurrentPage, queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import BoardContent from '~/boards/components/board_content.vue';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import BoardTopBar from '~/boards/components/board_top_bar.vue';
import { listsQuery } from 'ee_else_ce/boards/constants';
import { formatBoardLists } from 'ee_else_ce/boards/boards_util';
import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
export default {
i18n: {
fetchError: s__(
'Boards|An error occurred while fetching the board lists. Please reload the page.',
),
},
components: {
BoardContent,
BoardSettingsSidebar,
BoardTopBar,
},
inject: ['initialBoardId', 'initialFilterParams', 'isIssueBoard', 'isApolloBoard'],
inject: [
'fullPath',
'initialBoardId',
'initialFilterParams',
'isIssueBoard',
'isGroupBoard',
'issuableType',
'boardType',
'isApolloBoard',
],
data() {
return {
activeListId: '',
boardId: this.initialBoardId,
filterParams: { ...this.initialFilterParams },
isShowingEpicsSwimlanes: Boolean(queryToObject(window.location.search).group_by),
apolloError: null,
};
},
apollo: {
@ -38,10 +56,39 @@ export default {
return !this.isApolloBoard;
},
},
boardListsApollo: {
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);
},
error() {
this.apolloError = this.$options.i18n.fetchError;
},
},
},
computed: {
...mapGetters(['isSidebarOpen']),
listQueryVariables() {
return {
...(this.isIssueBoard && {
isGroup: this.isGroupBoard,
isProject: !this.isGroupBoard,
}),
fullPath: this.fullPath,
boardId: this.boardId,
filters: this.filterParams,
};
},
isSwimlanesOn() {
return (gon?.licensed_features?.swimlanes && this.isShowingEpicsSwimlanes) ?? false;
},
@ -51,6 +98,9 @@ export default {
}
return this.isSidebarOpen;
},
activeList() {
return this.activeListId ? this.boardListsApollo[this.activeListId] : undefined;
},
},
created() {
window.addEventListener('popstate', refreshCurrentPage);
@ -85,14 +135,20 @@ export default {
@toggleSwimlanes="isShowingEpicsSwimlanes = $event"
/>
<board-content
v-if="!isApolloBoard || boardListsApollo"
:board-id="boardId"
:is-swimlanes-on="isSwimlanesOn"
:filter-params="filterParams"
:board-lists-apollo="boardListsApollo"
:apollo-error="apolloError"
@setActiveList="setActiveId"
/>
<board-settings-sidebar
v-if="!isApolloBoard || activeList"
:list="activeList"
:list-id="activeListId"
:board-id="boardId"
:query-variables="listQueryVariables"
@unsetActiveId="setActiveId('')"
/>
</div>

View File

@ -5,20 +5,13 @@ import { sortBy, throttle } from 'lodash';
import Draggable from 'vuedraggable';
import { mapState, mapActions } from 'vuex';
import { contentTop } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import eventHub from '~/boards/eventhub';
import { formatBoardLists } from 'ee_else_ce/boards/boards_util';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import { defaultSortableOptions } from '~/sortable/constants';
import { DraggableItemTypes, listsQuery } from 'ee_else_ce/boards/constants';
import { DraggableItemTypes } from 'ee_else_ce/boards/constants';
import BoardColumn from './board_column.vue';
export default {
i18n: {
fetchError: s__(
'Boards|An error occurred while fetching the board lists. Please reload the page.',
),
},
draggableItemTypes: DraggableItemTypes,
components: {
BoardAddNewColumn,
@ -29,17 +22,7 @@ export default {
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert,
},
inject: [
'canAdminList',
'boardType',
'fullPath',
'issuableType',
'isIssueBoard',
'isEpicBoard',
'isGroupBoard',
'disabled',
'isApolloBoard',
],
inject: ['canAdminList', 'isIssueBoard', 'isEpicBoard', 'disabled', 'isApolloBoard'],
props: {
boardId: {
type: String,
@ -53,56 +36,27 @@ export default {
type: Boolean,
required: true,
},
boardListsApollo: {
type: Object,
required: false,
default: () => {},
},
apolloError: {
type: String,
required: false,
default: null,
},
},
data() {
return {
boardHeight: null,
boardListsApollo: {},
apolloError: null,
updatedBoardId: this.boardId,
};
},
apollo: {
boardListsApollo: {
query() {
return listsQuery[this.issuableType].query;
},
variables() {
return this.queryVariables;
},
skip() {
return !this.isApolloBoard;
},
update(data) {
const { lists } = data[this.boardType].board;
return formatBoardLists(lists);
},
result() {
// this allows us to delay fetching lists when we switch a board to fetch the actual board lists
// instead of fetching lists for the "previous" board
this.updatedBoardId = this.boardId;
},
error() {
this.apolloError = this.$options.i18n.fetchError;
},
},
},
computed: {
...mapState(['boardLists', 'error', 'addColumnForm']),
addColumnFormVisible() {
return this.addColumnForm?.visible;
},
queryVariables() {
return {
...(this.isIssueBoard && {
isGroup: this.isGroupBoard,
isProject: !this.isGroupBoard,
}),
fullPath: this.fullPath,
boardId: this.boardId,
filters: this.filterParams,
};
},
boardListsToUse() {
const lists = this.isApolloBoard ? this.boardListsApollo : this.boardLists;
return sortBy([...Object.values(lists)], 'position');

View File

@ -1,8 +1,15 @@
<script>
import { GlButton, GlDrawer, GlLabel, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui';
import produce from 'immer';
import { GlButton, GlDrawer, GlLabel, GlModal, GlModalDirective } from '@gitlab/ui';
import { MountingPortal } from 'portal-vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { LIST, ListType, ListTypeTitles, listsQuery } from 'ee_else_ce/boards/constants';
import {
LIST,
ListType,
ListTypeTitles,
listsQuery,
deleteListQueries,
} from 'ee_else_ce/boards/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
@ -21,7 +28,6 @@ export default {
GlModal,
GlDrawer,
GlLabel,
GlLoadingIcon,
MountingPortal,
BoardSettingsSidebarWipLimit: () =>
import('ee_component/boards/components/board_settings_wip_limit.vue'),
@ -35,11 +41,9 @@ export default {
inject: [
'boardType',
'canAdminList',
'fullPath',
'issuableType',
'scopedLabelsAvailable',
'isIssueBoard',
'isGroupBoard',
'isApolloBoard',
],
inheritAttrs: false,
@ -52,57 +56,33 @@ export default {
type: String,
required: true,
},
list: {
type: Object,
required: false,
default: () => null,
},
queryVariables: {
type: Object,
required: true,
},
},
data() {
return {
ListType,
list: {},
};
},
modalId: 'board-settings-sidebar-modal',
apollo: {
list: {
query() {
return listsQuery[this.issuableType].query;
},
variables() {
return this.queryVariables;
},
update(data) {
const { lists } = data[this.boardType].board;
return lists.nodes[0];
},
skip() {
return !this.isApolloBoard || !this.listId;
},
error() {
this.error = this.$options.i18n.fetchError;
},
},
},
computed: {
...mapGetters(['isSidebarOpen']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
isWipLimitsOn() {
return this.glFeatures.wipLimits && this.isIssueBoard;
},
queryVariables() {
return {
...(this.isIssueBoard && {
isGroup: this.isGroupBoard,
isProject: !this.isGroupBoard,
}),
fullPath: this.fullPath,
boardId: this.boardId,
filters: this.filterParams,
listId: this.activeListId,
};
},
activeListId() {
return this.isApolloBoard ? this.listId : this.activeId;
},
activeList() {
return this.isApolloBoard ? this.list : this.boardLists[this.activeId] || {};
return (this.isApolloBoard ? this.list : this.boardLists[this.activeId]) || {};
},
activeListLabel() {
return this.activeList.label;
@ -119,9 +99,6 @@ export default {
}
return this.sidebarType === LIST && this.isSidebarOpen;
},
isLoading() {
return this.isApolloBoard && this.$apollo.queries.list.loading;
},
},
created() {
eventHub.$on('sidebar.closeAll', this.unsetActiveListId);
@ -132,14 +109,18 @@ export default {
methods: {
...mapActions(['unsetActiveId', 'removeList']),
handleModalPrimary() {
this.deleteBoard();
this.deleteBoardList();
},
showScopedLabels(label) {
return this.scopedLabelsAvailable && isScopedLabel(label);
},
deleteBoard() {
async deleteBoardList() {
this.track('click_button', { label: 'remove_list' });
this.removeList(this.activeId);
if (this.isApolloBoard) {
await this.deleteList(this.activeListId);
} else {
this.removeList(this.activeId);
}
this.unsetActiveListId();
},
unsetActiveListId() {
@ -149,6 +130,25 @@ export default {
this.unsetActiveId();
}
},
async deleteList(listId) {
await this.$apollo.mutate({
mutation: deleteListQueries[this.issuableType].mutation,
variables: {
listId,
},
update: (store) => {
store.updateQuery(
{ query: listsQuery[this.issuableType].query, variables: this.queryVariables },
(sourceData) =>
produce(sourceData, (draftData) => {
draftData[this.boardType].board.lists.nodes = draftData[
this.boardType
].board.lists.nodes.filter((list) => list.id !== listId);
}),
);
},
});
},
},
};
</script>
@ -166,9 +166,8 @@ export default {
<h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">
{{ $options.listSettingsText }}
</h2>
<gl-loading-icon v-if="isLoading" />
</template>
<template v-if="!isLoading" #header>
<template #header>
<div v-if="canAdminList && activeList.id" class="gl-mt-3">
<gl-button
v-gl-modal="$options.modalId"
@ -179,7 +178,7 @@ export default {
</gl-button>
</div>
</template>
<template v-if="showSidebar && !isLoading">
<template v-if="showSidebar">
<div v-if="boardListType === ListType.label">
<label class="js-list-label gl-display-block">{{ listTypeTitle }}</label>
<gl-label

View File

@ -144,6 +144,9 @@ export default {
},
methods: {
...mapActions(['setError', 'fetchBoard', 'unsetActiveId']),
fullBoardId(boardId) {
return fullBoardId(boardId);
},
showPage(page) {
this.currentPage = page;
},
@ -254,7 +257,8 @@ export default {
if (isMetaKey(e)) {
window.open(`${this.boardBaseUrl}/${boardId}`, '_blank');
} else if (this.isApolloBoard) {
this.$emit('switchBoard', fullBoardId(boardId));
// Epic board ID is supported in EE version of this file
this.$emit('switchBoard', this.fullBoardId(boardId));
updateHistory({ url: `${this.boardBaseUrl}/${boardId}` });
} else {
this.unsetActiveId();

View File

@ -1,11 +1,10 @@
<script>
import { GlButton, GlFormInput, GlModal, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { GlDisclosureDropdown, GlButton, GlFormInput, GlModal, GlSprintf } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { sprintf, s__, __ } from '~/locale';
export const i18n = {
deleteButtonText: s__('Branches|Delete merged branches'),
buttonTooltipText: s__("Branches|Delete all branches that are merged into '%{defaultBranch}'"),
modalTitle: s__('Branches|Delete all merged branches?'),
modalMessage: s__(
'Branches|You are about to %{strongStart}delete all branches%{strongEnd} that were merged into %{codeStart}%{defaultBranch}%{codeEnd}.',
@ -28,14 +27,12 @@ export const i18n = {
export default {
csrf,
components: {
GlModal,
GlDisclosureDropdown,
GlButton,
GlModal,
GlFormInput,
GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
formPath: {
type: String,
@ -53,9 +50,6 @@ export default {
};
},
computed: {
buttonTooltipText() {
return sprintf(this.$options.i18n.buttonTooltipText, { defaultBranch: this.defaultBranch });
},
modalMessage() {
return sprintf(this.$options.i18n.modalMessage, {
defaultBranch: this.defaultBranch,
@ -67,6 +61,20 @@ export default {
isDeleteButtonDisabled() {
return !this.isDeletingConfirmed;
},
dropdownItems() {
return [
{
text: this.$options.i18n.deleteButtonText,
action: () => {
this.openModal();
},
extraAttrs: {
'data-qa-selector': 'delete_merged_branches_button',
class: 'gl-text-red-500!',
},
},
];
},
},
methods: {
openModal() {
@ -87,15 +95,15 @@ export default {
<template>
<div>
<gl-button
v-gl-tooltip="buttonTooltipText"
class="gl-mr-3"
data-qa-selector="delete_merged_branches_button"
category="secondary"
variant="danger"
@click="openModal"
>{{ $options.i18n.deleteButtonText }}
</gl-button>
<gl-disclosure-dropdown
:toggle-text="$options.i18n.actionsToggleText"
text-sr-only
icon="ellipsis_v"
category="tertiary"
no-caret
placement="right"
:items="dropdownItems"
/>
<gl-modal
ref="modal"
size="sm"

View File

@ -9,6 +9,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
include MetricsDashboard
include ProductAnalyticsTracking
include KasCookie
layout 'project'
@ -32,6 +33,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics, :cancel_auto_stop]
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
before_action :set_kas_cookie, only: [:index], if: -> { current_user }
after_action :expire_etag_cache, only: [:cancel_auto_stop]
track_event :index, :folder, :show, :new, :edit, :create, :update, :stop, :cancel_auto_stop, :terminal,

View File

@ -24,11 +24,6 @@
sorted_by: @sort }
}
- if can_push_code
.js-delete-merged-branches{ data: {
default_branch: @project.repository.root_ref,
form_path: project_merged_branches_path(@project) }
}
- if is_branch_rules_available
= link_to project_settings_repository_path(@project, anchor: 'js-branch-rules'), class: 'gl-button btn btn-default' do
= s_('Branches|View branch rules')
@ -36,6 +31,10 @@
- if can_push_code
= link_to new_project_branch_path(@project), class: 'gl-button btn btn-confirm' do
= s_('Branches|New branch')
.js-delete-merged-branches{ data: {
default_branch: @project.repository.root_ref,
form_path: project_merged_branches_path(@project) }
}
= render_if_exists 'projects/commits/mirror_status'

View File

@ -1,8 +0,0 @@
---
name: environment_search_api_min_chars
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108277
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/387244
milestone: '15.8'
type: development
group: group::configure
default_enabled: false

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/381456
milestone: '15.7'
type: development
group: group::foundations
default_enabled: false
default_enabled: true

View File

@ -3,8 +3,8 @@ data_category: optional
key_path: redis_hll_counters.deploy_token_packages.i_package_terraform_module_deploy_token_monthly
description: Number of distinct users authorized via deploy token creating Terraform Module packages in recent 28 days
product_section: ops
product_stage: configure
product_group: configure
product_stage: package
product_group: package
value_type: number
status: active
milestone: '13.11'

View File

@ -3,8 +3,8 @@ data_category: optional
key_path: redis_hll_counters.user_packages.i_package_terraform_module_user_monthly
description: Number of distinct users creating Terraform Module packages in recent 28 days
product_section: ops
product_stage: configure
product_group: configure
product_stage: package
product_group: package
value_type: number
status: active
milestone: '13.11'

View File

@ -3,8 +3,8 @@ data_category: optional
key_path: counts.package_events_i_package_terraform_module_delete_package
description: Total count of Terraform Module packages delete events
product_section: ops
product_stage: configure
product_group: configure
product_stage: package
product_group: package
value_type: number
status: active
milestone: '13.11'

View File

@ -3,8 +3,8 @@ data_category: optional
key_path: counts.package_events_i_package_terraform_module_pull_package
description: Total count of pull Terraform Module packages events
product_section: ops
product_stage: configure
product_group: configure
product_stage: package
product_group: package
value_type: number
status: active
milestone: '13.11'

View File

@ -3,8 +3,8 @@ data_category: optional
key_path: counts.package_events_i_package_terraform_module_push_package
description: Total count of push Terraform Module packages events
product_section: ops
product_stage: configure
product_group: configure
product_stage: package
product_group: package
value_type: number
status: active
milestone: '13.11'

View File

@ -1031,6 +1031,7 @@ Input type: `AiActionInput`
| ---- | ---- | ----------- |
| <a id="mutationaiactionclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationaiactionerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationaiactionrequestid"></a>`requestId` | [`String`](#string) | ID of the request. |
### `Mutation.alertSetAssignees`
@ -11367,6 +11368,7 @@ Information about a connected Agent.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="airesponseerrors"></a>`errors` | [`[String!]`](#string) | Errors return by AI API as response. |
| <a id="airesponserequestid"></a>`requestId` | [`String`](#string) | ID of the original request. |
| <a id="airesponseresponsebody"></a>`responseBody` | [`String`](#string) | Response body from AI API. |
### `AlertManagementAlert`

View File

@ -65,7 +65,7 @@ sequenceDiagram
Note right of Cloud: Create role with conditionals
Note left of GitLab: CI/CD job with ID token
GitLab->>+Cloud: Call cloud API with ID token
Note right of Cloud: Decode & verify JWT with public key (https://gitlab/-/jwks)
Note right of Cloud: Decode & verify JWT with public key (https://gitlab.com/oauth/discovery/keys)
Note right of Cloud: Validate audience defined in OIDC
Note right of Cloud: Validate conditional (sub, aud) role
Note right of Cloud: Generate credential or fetch secret

View File

@ -29,12 +29,11 @@ restore. This is because the system user performing the restore actions (`git`)
is usually not allowed to create or delete the SQL database needed to import
data into (`gitlabhq_production`). All existing data is either erased
(SQL) or moved to a separate directory (such as repositories and uploads).
Restoring SQL data skips views owned by PostgreSQL extensions.
To restore a backup, **you must also restore the GitLab secrets**.
These include the database encryption key, [CI/CD variables](../ci/variables/index.md), and
variables used for [two-factor authentication](../user/profile/account/two_factor_authentication.md).
Without the keys, [multiple issues occur](backup_restore.md#when-the-secrets-file-is-lost),
including loss of access by users with [two-factor authentication enabled](../user/profile/account/two_factor_authentication.md),
and GitLab Runners cannot log in.

View File

@ -78,6 +78,7 @@ The following stage events are available:
- Issue closed
- Issue created
- Issue first added to board
- Issue first assigned
- Issue first associated with milestone
- Issue first mentioned
- Issue label added
@ -86,6 +87,7 @@ The following stage events are available:
- MR merged
- MR created
- MR first commit time
- MR first assigned
- MR first deployed
- MR label added
- MR label removed

View File

@ -48,7 +48,7 @@ module API
get ':id/environments' do
authorize! :read_environment, user_project
if Feature.enabled?(:environment_search_api_min_chars, user_project) && params[:search].present? && params[:search].length < MIN_SEARCH_LENGTH
if params[:search].present? && params[:search].length < MIN_SEARCH_LENGTH
bad_request!("Search query is less than #{MIN_SEARCH_LENGTH} characters")
end

View File

@ -326,7 +326,7 @@ module Feature
end
def l2_cache_backend
Rails.cache
::Gitlab::Redis::FeatureFlag.cache_store
end
def log(key:, action:, **extra)

View File

@ -10,6 +10,7 @@ module Gitlab
ALL_CLASSES = [
Gitlab::Redis::Cache,
Gitlab::Redis::DbLoadBalancing,
Gitlab::Redis::FeatureFlag,
Gitlab::Redis::Queues,
Gitlab::Redis::RateLimiting,
Gitlab::Redis::RepositoryCache,

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Gitlab
module Redis
class FeatureFlag < ::Gitlab::Redis::Wrapper
FeatureFlagStore = Class.new(ActiveSupport::Cache::RedisCacheStore)
class << self
# The data we store on FeatureFlag is currently stored on Cache.
def config_fallback
Cache
end
def cache_store
@cache_store ||= FeatureFlagStore.new(
redis: pool,
compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')),
namespace: Cache::CACHE_NAMESPACE,
expires_in: 1.hour
)
end
end
end
end
end

View File

@ -7903,9 +7903,6 @@ msgstr ""
msgid "Branches|Compare"
msgstr ""
msgid "Branches|Delete all branches that are merged into '%{defaultBranch}'"
msgstr ""
msgid "Branches|Delete all merged branches?"
msgstr ""

View File

@ -144,8 +144,8 @@ describe('MessageForm', () => {
findForm().vm.$emit('submit', { preventDefault: () => {} });
await waitForPromises();
expect(axiosMock.history.post).toHaveLength(1);
expect(axiosMock.history.post[0]).toMatchObject({
expect(axiosMock.history.post).toHaveLength(2);
expect(axiosMock.history.post[1]).toMatchObject({
url: messagesPath,
data: JSON.stringify(defaultPayload),
});

View File

@ -6,12 +6,14 @@ import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
import BoardApp from '~/boards/components/board_app.vue';
import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import { rawIssue } from '../mock_data';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import { rawIssue, boardListsQueryResponse } from '../mock_data';
describe('BoardApp', () => {
let wrapper;
let store;
const mockApollo = createMockApollo();
const boardListQueryHandler = jest.fn().mockResolvedValue(boardListsQueryResponse);
const mockApollo = createMockApollo([[boardListsQuery, boardListQueryHandler]]);
Vue.use(Vuex);
Vue.use(VueApollo);
@ -41,9 +43,13 @@ describe('BoardApp', () => {
apolloProvider: mockApollo,
store,
provide: {
fullPath: 'gitlab-org',
initialBoardId: 'gid://gitlab/Board/1',
initialFilterParams: {},
issuableType: 'issue',
boardType: 'group',
isIssueBoard: true,
isGroupBoard: true,
isApolloBoard,
},
});
@ -73,6 +79,10 @@ describe('BoardApp', () => {
await nextTick();
});
it('fetches lists', () => {
expect(boardListQueryHandler).toHaveBeenCalled();
});
it('should have is-compact class when a card is selected', () => {
expect(wrapper.classes()).toContain('is-compact');
});

View File

@ -1,23 +1,19 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Draggable from 'vuedraggable';
import Vuex from 'vuex';
import eventHub from '~/boards/eventhub';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import getters from 'ee_else_ce/boards/stores/getters';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import BoardColumn from '~/boards/components/board_column.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import { mockLists, boardListsQueryResponse } from '../mock_data';
import { mockLists, mockListsById } from '../mock_data';
Vue.use(VueApollo);
Vue.use(Vuex);
const actions = {
@ -26,7 +22,6 @@ const actions = {
describe('BoardContent', () => {
let wrapper;
let fakeApollo;
const defaultState = {
isShowingEpicsSwimlanes: false,
@ -51,26 +46,21 @@ describe('BoardContent', () => {
issuableType = 'issue',
isIssueBoard = true,
isEpicBoard = false,
boardListQueryHandler = jest.fn().mockResolvedValue(boardListsQueryResponse),
} = {}) => {
fakeApollo = createMockApollo([[boardListsQuery, boardListQueryHandler]]);
const store = createStore({
...defaultState,
...state,
});
wrapper = shallowMount(BoardContent, {
apolloProvider: fakeApollo,
propsData: {
boardId: 'gid://gitlab/Board/1',
filterParams: {},
isSwimlanesOn: false,
boardListsApollo: mockListsById,
...props,
},
provide: {
canAdminList,
boardType: 'group',
fullPath: 'gitlab-org/gitlab',
issuableType,
isIssueBoard,
isEpicBoard,
@ -110,10 +100,6 @@ describe('BoardContent', () => {
};
});
afterEach(() => {
fakeApollo = null;
});
describe('default', () => {
beforeEach(() => {
createComponent();

View File

@ -10,12 +10,12 @@ import { stubComponent } from 'helpers/stub_component';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import { inactiveId, LIST } from '~/boards/constants';
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
import actions from '~/boards/stores/actions';
import getters from '~/boards/stores/getters';
import mutations from '~/boards/stores/mutations';
import sidebarEventHub from '~/sidebar/event_hub';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import { mockLabelList, issueBoardListsQueryResponse } from '../mock_data';
import { mockLabelList, destroyBoardListMutationResponse } from '../mock_data';
Vue.use(VueApollo);
Vue.use(Vuex);
@ -28,7 +28,9 @@ describe('BoardSettingsSidebar', () => {
const listId = mockLabelList.id;
const modalID = 'board-settings-sidebar-modal';
const boardListQueryHandler = jest.fn().mockResolvedValue(issueBoardListsQueryResponse);
const destroyBoardListMutationHandlerSuccess = jest
.fn()
.mockResolvedValue(destroyBoardListMutationResponse);
const createComponent = ({
canAdminList = false,
@ -46,25 +48,28 @@ describe('BoardSettingsSidebar', () => {
mutations,
actions,
});
mockApollo = createMockApollo([[boardListsQuery, boardListQueryHandler]]);
mockApollo = createMockApollo([
[destroyBoardListMutation, destroyBoardListMutationHandlerSuccess],
]);
wrapper = extendedWrapper(
shallowMount(BoardSettingsSidebar, {
apolloProvider: mockApollo,
store,
apolloProvider: mockApollo,
provide: {
canAdminList,
scopedLabelsAvailable: false,
isIssueBoard: true,
boardType: 'group',
fullPath: 'gitlab-org',
issuableType: 'issue',
isGroupBoard: true,
isApolloBoard,
},
propsData: {
listId: 'gid://gitlab/List/1',
listId: list.id || '',
boardId: 'gid://gitlab/Board/1',
list,
queryVariables: {},
},
directives: {
GlModal: createMockDirective('gl-modal'),
@ -76,6 +81,9 @@ describe('BoardSettingsSidebar', () => {
},
}),
);
// Necessary for cache update
mockApollo.clients.defaultClient.cache.updateQuery = jest.fn();
};
const findLabel = () => wrapper.findComponent(GlLabel);
const findDrawer = () => wrapper.findComponent(GlDrawer);
@ -185,6 +193,21 @@ describe('BoardSettingsSidebar', () => {
expect(findRemoveButton().exists()).toBe(true);
});
it('removes the list', () => {
createComponent({
canAdminList: true,
activeId: listId,
list: mockLabelList,
isApolloBoard: true,
});
findRemoveButton().vm.$emit('click');
wrapper.findComponent(GlModal).vm.$emit('primary');
expect(destroyBoardListMutationHandlerSuccess).toHaveBeenCalled();
});
it('has the correct ID on the button', () => {
createComponent({ canAdminList: true, activeId: listId, list: mockLabelList });
const binding = getBinding(findRemoveButton().element, 'gl-modal');
@ -196,12 +219,4 @@ describe('BoardSettingsSidebar', () => {
expect(findModal().props('modalId')).toBe(modalID);
});
});
describe('Apollo boards', () => {
it('fetches list', () => {
createComponent({ isApolloBoard: true });
expect(boardListQueryHandler).toHaveBeenCalled();
});
});
});

View File

@ -997,4 +997,13 @@ export const updateBoardListResponse = {
},
};
export const destroyBoardListMutationResponse = {
data: {
destroyBoardList: {
errors: [],
__typename: 'DestroyBoardListPayload',
},
},
};
export const DEFAULT_COLOR = '#1068bf';

View File

@ -2,25 +2,33 @@
exports[`Delete merged branches component Delete merged branches confirmation modal matches snapshot 1`] = `
<div>
<b-button-stub
class="gl-mr-3 gl-button btn-danger-secondary"
data-qa-selector="delete_merged_branches_button"
size="md"
tag="button"
type="button"
variant="danger"
<gl-base-dropdown-stub
category="tertiary"
class="gl-disclosure-dropdown"
icon="ellipsis_v"
nocaret="true"
placement="right"
popperoptions="[object Object]"
size="medium"
textsronly="true"
toggleid="dropdown-toggle-btn-25"
toggletext=""
variant="default"
>
<!---->
<!---->
<span
class="gl-button-text"
<ul
aria-labelledby="dropdown-toggle-btn-25"
class="gl-new-dropdown-contents"
data-testid="disclosure-content"
id="disclosure-26"
tabindex="-1"
>
Delete merged branches
</span>
</b-button-stub>
<gl-disclosure-dropdown-item-stub
item="[object Object]"
/>
</ul>
</gl-base-dropdown-stub>
<div>
<form

View File

@ -1,9 +1,8 @@
import { GlButton, GlFormInput, GlModal, GlSprintf } from '@gitlab/ui';
import { GlDisclosureDropdown, GlButton, GlFormInput, GlModal, GlSprintf } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import DeleteMergedBranches, { i18n } from '~/branches/components/delete_merged_branches.vue';
import { formPath, propsDataMock } from '../mock_data';
@ -22,6 +21,7 @@ const stubsData = {
hide: modalHideSpy,
},
}),
GlDisclosureDropdown,
GlButton,
GlFormInput,
GlSprintf,
@ -32,14 +32,12 @@ const createComponent = (mountFn = shallowMountExtended, stubs = {}) => {
propsData: {
...propsDataMock,
},
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
stubs,
});
};
const findDeleteButton = () => wrapper.findComponent(GlButton);
const findDeleteButton = () =>
wrapper.findComponent('[data-qa-selector="delete_merged_branches_button"]');
const findModal = () => wrapper.findComponent(GlModal);
const findConfirmationButton = () =>
wrapper.findByTestId('delete-merged-branches-confirmation-button');
@ -54,22 +52,11 @@ describe('Delete merged branches component', () => {
});
describe('Delete merged branches button', () => {
it('has correct attributes, text and tooltip', () => {
expect(findDeleteButton().attributes()).toMatchObject({
category: 'secondary',
variant: 'danger',
});
it('has correct text', () => {
createComponent(mount, stubsData);
expect(findDeleteButton().text()).toBe(i18n.deleteButtonText);
});
it('displays a tooltip', () => {
const tooltip = getBinding(findDeleteButton().element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value).toBe(wrapper.vm.buttonTooltipText);
});
it('opens modal when clicked', () => {
createComponent(mount, stubsData);
findDeleteButton().trigger('click');
@ -130,7 +117,6 @@ describe('Delete merged branches component', () => {
it('submits form when correct amount is provided and the confirm button is clicked', async () => {
findFormInput().vm.$emit('input', 'delete');
await waitForPromises();
expect(findDeleteButton().props('disabled')).not.toBe(true);
findConfirmationButton().trigger('click');
expect(submitFormSpy()).toHaveBeenCalled();
});

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Feature, stub_feature_flags: false, feature_category: :shared do
RSpec.describe Feature, :clean_gitlab_redis_feature_flag, stub_feature_flags: false, feature_category: :shared do
include StubVersion
before do
@ -212,7 +212,7 @@ RSpec.describe Feature, stub_feature_flags: false, feature_category: :shared do
end
it { expect(described_class.send(:l1_cache_backend)).to eq(Gitlab::ProcessMemoryCache.cache_backend) }
it { expect(described_class.send(:l2_cache_backend)).to eq(Rails.cache) }
it { expect(described_class.send(:l2_cache_backend)).to eq(Gitlab::Redis::FeatureFlag.cache_store) }
it 'caches the status in L1 and L2 caches',
:request_store, :use_clean_rails_memory_store_caching do

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Redis::FeatureFlag, feature_category: :redis do
include_examples "redis_new_instance_shared_examples", 'feature_flag', Gitlab::Redis::Cache
describe '.cache_store' do
it 'has a default ttl of 1 hour' do
expect(described_class.cache_store.options[:expires_in]).to eq(1.hour)
end
end
end

View File

@ -72,30 +72,11 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
end
context "when params[:search] is less than #{described_class::MIN_SEARCH_LENGTH} characters" do
before do
stub_feature_flags(environment_search_api_min_chars: false)
end
it 'returns a normal response' do
it 'returns with status 400' do
get api("/projects/#{project.id}/environments?search=ab", user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(0)
end
context 'and environment_search_api_min_chars flag is enabled for the project' do
before do
stub_feature_flags(environment_search_api_min_chars: project)
end
it 'returns with status 400' do
get api("/projects/#{project.id}/environments?search=ab", user)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to include("Search query is less than #{described_class::MIN_SEARCH_LENGTH} characters")
end
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to include("Search query is less than #{described_class::MIN_SEARCH_LENGTH} characters")
end
end