diff options
21 files changed, 384 insertions, 13 deletions
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 3638fdd2ca5..192bc4b5d25 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -98,6 +98,6 @@ export default { class="board-card gl-p-5 gl-rounded-base" @click="toggleIssue($event)" > - <board-card-inner :list="list" :item="item" :update-filters="true" /> + <board-card-inner :list="list" :item="item" :update-filters="true" :index="index" /> </li> </template> diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue index 8dc521317cd..a9a4912d9e9 100644 --- a/app/assets/javascripts/boards/components/board_card_inner.vue +++ b/app/assets/javascripts/boards/components/board_card_inner.vue @@ -15,6 +15,7 @@ import { updateHistory } from '~/lib/utils/url_utility'; import { sprintf, __, n__ } from '~/locale'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; import { ListType } from '../constants'; import eventHub from '../eventhub'; import BoardBlockedIcon from './board_blocked_icon.vue'; @@ -34,6 +35,7 @@ export default { IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'), BoardBlockedIcon, GlSprintf, + BoardCardMoveToPosition, }, directives: { GlTooltip: GlTooltipDirective, @@ -55,6 +57,10 @@ export default { required: false, default: false, }, + index: { + type: Number, + required: true, + }, }, data() { return { @@ -202,7 +208,7 @@ export default { <template> <div> <div class="gl-display-flex" dir="auto"> - <h4 class="board-card-title gl-mb-0 gl-mt-0"> + <h4 class="board-card-title gl-mb-0 gl-mt-0 gl-mr-3"> <board-blocked-icon v-if="item.blocked" :item="item" @@ -235,6 +241,7 @@ export default { >{{ item.title }}</a > </h4> + <board-card-move-to-position :item="item" :list="list" :index="index" /> </div> <div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap"> <template v-for="label in orderedLabels"> diff --git a/app/assets/javascripts/boards/components/board_card_move_to_position.vue b/app/assets/javascripts/boards/components/board_card_move_to_position.vue new file mode 100644 index 00000000000..053dbb62b2e --- /dev/null +++ b/app/assets/javascripts/boards/components/board_card_move_to_position.vue @@ -0,0 +1,140 @@ +<script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { mapActions, mapGetters } from 'vuex'; +import { s__ } from '~/locale'; +import { DEFAULT_BOARD_LIST_ITEMS_SIZE } from 'ee_else_ce/boards/constants'; + +import Tracking from '~/tracking'; + +export default { + i18n: { + moveToStartText: s__('Boards|Move to start of list'), + moveToEndText: s__('Boards|Move to end of list'), + }, + name: 'BoardCardMoveToPosition', + components: { + GlDropdown, + GlDropdownItem, + }, + mixins: [Tracking.mixin()], + props: { + item: { + type: Object, + required: true, + validator: (item) => ['id', 'iid', 'referencePath'].every((key) => item[key]), + }, + list: { + type: Object, + required: false, + default: () => ({}), + }, + index: { + type: Number, + required: true, + }, + }, + computed: { + ...mapGetters(['getBoardItemsByList']), + tracking() { + return { + category: 'boards:list', + label: 'move_to_position', + property: `type_card`, + }; + }, + listItems() { + return this.getBoardItemsByList(this.list.id); + }, + firstItemInListId() { + return this.listItems[0]?.id; + }, + lengthOfListItemsInBoard() { + return this.listItems?.length; + }, + lastItemInTheListId() { + return this.listItems[this.lengthOfListItemsInBoard - 1]?.id; + }, + itemIdentifier() { + return `${this.item.id}-${this.item.iid}-${this.index}`; + }, + showMoveToEndOfList() { + return this.lengthOfListItemsInBoard <= DEFAULT_BOARD_LIST_ITEMS_SIZE; + }, + isFirstItemInList() { + return this.index === 0; + }, + isLastItemInList() { + return this.index === this.lengthOfListItemsInBoard - 1; + }, + }, + methods: { + ...mapActions(['moveItem']), + moveToStart() { + this.track('click_toggle_button', { + label: 'move_to_start', + }); + /** in case it is the first in the list don't call any action/mutation * */ + if (this.isFirstItemInList) { + return; + } + const moveAfterId = this.firstItemInListId; + this.moveToPosition({ + moveAfterId, + }); + }, + moveToEnd() { + this.track('click_toggle_button', { + label: 'move_to_end', + }); + /** in case it is the last in the list don't call any action/mutation * */ + if (this.isLastItemInList) { + return; + } + const moveBeforeId = this.lastItemInTheListId; + this.moveToPosition({ + moveBeforeId, + }); + }, + moveToPosition({ moveAfterId, moveBeforeId }) { + this.moveItem({ + itemId: this.item.id, + itemIid: this.item.iid, + itemPath: this.item.referencePath, + fromListId: this.list.id, + toListId: this.list.id, + moveAfterId, + moveBeforeId, + }); + }, + }, +}; +</script> + +<template> + <gl-dropdown + ref="dropdown" + :key="itemIdentifier" + data-testid="move-card-dropdown" + icon="ellipsis_v" + :text="s__('Boards|Move card')" + :text-sr-only="true" + class="move-to-position gl-display-block gl-mb-2 gl-ml-2 gl-mt-n3 gl-mr-n3" + category="tertiary" + :tabindex="index" + no-caret + @keydown.esc.native="$emit('hide')" + > + <div> + <gl-dropdown-item data-testid="action-move-to-first" @click.stop="moveToStart"> + {{ $options.i18n.moveToStartText }} + </gl-dropdown-item> + <gl-dropdown-item + v-if="showMoveToEndOfList" + data-testid="action-move-to-end" + @click.stop="moveToEnd" + > + {{ $options.i18n.moveToEndText }} + </gl-dropdown-item> + </div> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js index 0f290f566ba..d745eed556f 100644 --- a/app/assets/javascripts/boards/constants.js +++ b/app/assets/javascripts/boards/constants.js @@ -146,3 +146,5 @@ export default { BoardType, ListType, }; + +export const DEFAULT_BOARD_LIST_ITEMS_SIZE = 10; diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index 791182af806..f84274104b2 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -15,6 +15,7 @@ import { FilterFields, ListTypeTitles, DraggableItemTypes, + DEFAULT_BOARD_LIST_ITEMS_SIZE, } from 'ee_else_ce/boards/constants'; import { formatIssueInput, @@ -429,7 +430,7 @@ export default { filters: filterParams, isGroup: boardType === BoardType.group, isProject: boardType === BoardType.project, - first: 10, + first: DEFAULT_BOARD_LIST_ITEMS_SIZE, after: fetchNext ? state.pageInfoByListId[listId].endCursor : undefined, }; diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss index 197073412e8..5717032a326 100644 --- a/app/assets/stylesheets/page_bundles/boards.scss +++ b/app/assets/stylesheets/page_bundles/boards.scss @@ -207,6 +207,14 @@ background-color: var(--blue-50, $blue-50); } + .move-to-position { + visibility: hidden; + } + + &:hover .move-to-position { + visibility: visible; + } + &.multi-select { border-color: var(--blue-200, $blue-200); background-color: var(--blue-50, $blue-50); @@ -234,11 +242,18 @@ @include media-breakpoint-down(md) { padding: $gl-padding-8; } + + @include media-breakpoint-down(sm) { + .move-to-position { + visibility: visible; + } + } } .board-card-title { @include overflow-break-word(); font-size: 1em; + width: 95%; a { color: var(--gray-900, $gray-900); diff --git a/app/graphql/mutations/custom_emoji/create.rb b/app/graphql/mutations/custom_emoji/create.rb index 269ea6c9999..535ff44a7fd 100644 --- a/app/graphql/mutations/custom_emoji/create.rb +++ b/app/graphql/mutations/custom_emoji/create.rb @@ -28,6 +28,10 @@ module Mutations description: 'Location of the emoji file.' def resolve(group_path:, **args) + if Feature.disabled?(:custom_emoji) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Custom emoji feature is disabled' + end + group = authorized_find!(group_path: group_path) # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37911#note_444682238 args[:external] = true diff --git a/app/graphql/mutations/custom_emoji/destroy.rb b/app/graphql/mutations/custom_emoji/destroy.rb index 863b8152cc7..64e3f2ed7d3 100644 --- a/app/graphql/mutations/custom_emoji/destroy.rb +++ b/app/graphql/mutations/custom_emoji/destroy.rb @@ -17,6 +17,10 @@ module Mutations description: 'Global ID of the custom emoji to destroy.' def resolve(id:) + if Feature.disabled?(:custom_emoji) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Custom emoji feature is disabled' + end + custom_emoji = authorized_find!(id: id) custom_emoji.destroy! diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index 3a7507b038f..8542e611f02 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -22,7 +22,7 @@ module Types type: Types::CustomEmojiType.connection_type, null: true, description: 'Custom emoji within this namespace.', - _deprecated_feature_flag: :custom_emoji + alpha: { milestone: '13.6' } field :share_with_group_lock, type: GraphQL::Types::Boolean, @@ -278,6 +278,10 @@ module Types group.dependency_proxy_setting || group.create_dependency_proxy_setting end + def custom_emoji + object.custom_emoji if Feature.enabled?(:custom_emoji) + end + private def group diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 499c2e786bf..e1806e5b19a 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -37,8 +37,8 @@ module Types mount_mutation Mutations::Clusters::AgentTokens::Create mount_mutation Mutations::Clusters::AgentTokens::Revoke mount_mutation Mutations::Commits::Create, calls_gitaly: true - mount_mutation Mutations::CustomEmoji::Create, _deprecated_feature_flag: :custom_emoji - mount_mutation Mutations::CustomEmoji::Destroy, _deprecated_feature_flag: :custom_emoji + mount_mutation Mutations::CustomEmoji::Create, alpha: { milestone: '13.6' } + mount_mutation Mutations::CustomEmoji::Destroy, alpha: { milestone: '13.6' } mount_mutation Mutations::CustomerRelations::Contacts::Create mount_mutation Mutations::CustomerRelations::Contacts::Update mount_mutation Mutations::CustomerRelations::Organizations::Create diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index a0c228a6944..3a2aa1bde92 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1409,7 +1409,9 @@ Input type: `CreateComplianceFrameworkInput` ### `Mutation.createCustomEmoji` -Available only when feature flag `custom_emoji` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. +WARNING: +**Introduced** in 13.6. +This feature is in Alpha. It can be changed or removed at any time. Input type: `CreateCustomEmojiInput` @@ -2271,7 +2273,9 @@ Input type: `DestroyContainerRepositoryTagsInput` ### `Mutation.destroyCustomEmoji` -Available only when feature flag `custom_emoji` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. +WARNING: +**Introduced** in 13.6. +This feature is in Alpha. It can be changed or removed at any time. Input type: `DestroyCustomEmojiInput` @@ -12144,7 +12148,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="groupcontainerrepositoriescount"></a>`containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the group. | | <a id="groupcontainslockedprojects"></a>`containsLockedProjects` | [`Boolean!`](#boolean) | Includes at least one project where the repository size exceeds the limit. | | <a id="groupcrossprojectpipelineavailable"></a>`crossProjectPipelineAvailable` | [`Boolean!`](#boolean) | Indicates if the cross_project_pipeline feature is available for the namespace. | -| <a id="groupcustomemoji"></a>`customEmoji` | [`CustomEmojiConnection`](#customemojiconnection) | Custom emoji within this namespace. Available only when feature flag `custom_emoji` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. (see [Connections](#connections)) | +| <a id="groupcustomemoji"></a>`customEmoji` **{warning-solid}** | [`CustomEmojiConnection`](#customemojiconnection) | **Introduced** in 13.6. This feature is in Alpha. It can be changed or removed at any time. Custom emoji within this namespace. | | <a id="groupdependencyproxyblobcount"></a>`dependencyProxyBlobCount` | [`Int!`](#int) | Number of dependency proxy blobs cached in the group. | | <a id="groupdependencyproxyblobs"></a>`dependencyProxyBlobs` | [`DependencyProxyBlobConnection`](#dependencyproxyblobconnection) | Dependency Proxy blobs. (see [Connections](#connections)) | | <a id="groupdependencyproxyimagecount"></a>`dependencyProxyImageCount` | [`Int!`](#int) | Number of dependency proxy images cached in the group. | diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md index 7c7d5380a24..b7c9d5e8102 100644 --- a/doc/user/application_security/index.md +++ b/doc/user/application_security/index.md @@ -497,6 +497,11 @@ Feedback is welcome on our vision for [unifying the user experience for these tw --> ### Secure job failing with exit code 1 +WARNING: +Debug logging can be a serious security risk. The output may contain the content of +environment variables and other secrets available to the job. The output is uploaded +to the GitLab server and visible in job logs. + If a Secure job is failing and it's unclear why, add `SECURE_LOG_LEVEL: "debug"` as a global CI/CD variable for more verbose output that is helpful for troubleshooting. @@ -534,6 +539,11 @@ Select **new pipeline** to run a new pipeline. ### Getting warning messages `… report.json: no matching files` +WARNING: +Debug logging can be a serious security risk. The output may contain the content of +environment variables and other secrets available to the job. The output is uploaded +to the GitLab server and visible in job logs. + This message is often followed by the [error `No files to upload`](../../ci/pipelines/job_artifacts.md#error-message-no-files-to-upload), and preceded by other errors or warnings that indicate why the JSON report wasn't generated. Check the entire job log for such messages. If you don't find these messages, retry the failed job after diff --git a/doc/user/ssh.md b/doc/user/ssh.md index 5667890757a..4911cce7fe9 100644 --- a/doc/user/ssh.md +++ b/doc/user/ssh.md @@ -419,6 +419,9 @@ as both have a different home directory: You can either copy over the `.ssh/` directory to use the same key, or generate a key in each environment. +If you're running Windows 11 and using [OpenSSH for Windows](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_overview), ensure the `HOME` +environment variable is set correctly. Otherwise, your private SSH key might not be found. + Alternative tools include: - [Cygwin](https://www.cygwin.com) @@ -468,6 +471,7 @@ This indicates that something is wrong with your SSH setup. - Try to manually register your private SSH key by using `ssh-agent`. - Try to debug the connection by running `ssh -Tv git@example.com`. Replace `example.com` with your GitLab URL. +- Ensure you followed all the instructions in [Use SSH on Microsoft Windows](#use-ssh-on-microsoft-windows). ### `Could not resolve hostname` error diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5b8d19d75e6..a0ded75e6ee 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6581,6 +6581,15 @@ msgstr "" msgid "Boards|Failed to fetch blocking %{issuableType}s" msgstr "" +msgid "Boards|Move card" +msgstr "" + +msgid "Boards|Move to end of list" +msgstr "" + +msgid "Boards|Move to start of list" +msgstr "" + msgid "Boards|New board" msgstr "" diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index 985902b4a3b..7be2d57220b 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -7,6 +7,7 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue'; import BoardCardInner from '~/boards/components/board_card_inner.vue'; +import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; import { issuableTypes } from '~/boards/constants'; import eventHub from '~/boards/eventhub'; import defaultStore from '~/boards/stores'; @@ -47,6 +48,7 @@ describe('Board card component', () => { const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight'); const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content'); const findHiddenIssueIcon = () => wrapper.findByTestId('hidden-icon'); + const findMoveToPositionComponent = () => wrapper.findComponent(BoardCardMoveToPosition); const performSearchMock = jest.fn(); @@ -75,10 +77,12 @@ describe('Board card component', () => { propsData: { list, item: issue, + index: 0, ...props, }, stubs: { GlLoadingIcon: true, + BoardCardMoveToPosition: true, }, directives: { GlTooltip: createMockDirective(), @@ -137,6 +141,10 @@ describe('Board card component', () => { expect(findHiddenIssueIcon().exists()).toBe(false); }); + it('renders the move to position icon', () => { + expect(findMoveToPositionComponent().exists()).toBe(true); + }); + it('renders issue ID with #', () => { expect(wrapper.find('.board-card-number').text()).toContain(`#${issue.iid}`); }); diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js index 04192489817..65a41c49e7f 100644 --- a/spec/frontend/boards/board_list_helper.js +++ b/spec/frontend/boards/board_list_helper.js @@ -75,6 +75,7 @@ export default function createComponent({ id: 1, iid: 1, confidential: false, + referencePath: 'gitlab-org/test-subgroup/gitlab-test#1', labels: [], assignees: [], ...listIssueProps, diff --git a/spec/frontend/boards/components/board_card_move_to_position_spec.js b/spec/frontend/boards/components/board_card_move_to_position_spec.js new file mode 100644 index 00000000000..f6c66baa6aa --- /dev/null +++ b/spec/frontend/boards/components/board_card_move_to_position_spec.js @@ -0,0 +1,117 @@ +import Vue, { nextTick } from 'vue'; +import Vuex from 'vuex'; +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; + +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; +import { createStore } from '~/boards/stores'; +import { mockList, mockIssue2 } from 'jest/boards/mock_data'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; + +Vue.use(Vuex); + +const dropdownOptions = [ + BoardCardMoveToPosition.i18n.moveToStartText, + BoardCardMoveToPosition.i18n.moveToEndText, +]; + +describe('Board Card Move to position', () => { + let wrapper; + let trackingSpy; + let store; + let dispatch; + + store = new Vuex.Store(); + + const createComponent = (propsData) => { + wrapper = shallowMountExtended(BoardCardMoveToPosition, { + store, + propsData: { + item: mockIssue2, + list: mockList, + index: 0, + ...propsData, + }, + stubs: { + GlDropdown, + GlDropdownItem, + }, + }); + }; + + beforeEach(() => { + store = createStore(); + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + const findEllipsesButton = () => wrapper.findByTestId('move-card-dropdown'); + const findMoveToPositionDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownItems = () => findMoveToPositionDropdown().findAllComponents(GlDropdownItem); + const findDropdownItemAtIndex = (index) => findDropdownItems().at(index); + + describe('Dropdown', () => { + describe('Dropdown button', () => { + it('has an icon with vertical ellipsis', () => { + expect(findEllipsesButton().exists()).toBe(true); + expect(findMoveToPositionDropdown().props('icon')).toBe('ellipsis_v'); + }); + + it('is opened on the click of vertical ellipsis and has 2 dropdown items when number of list items < 10', () => { + findMoveToPositionDropdown().vm.$emit('click'); + + expect(findDropdownItems()).toHaveLength(dropdownOptions.length); + }); + }); + + describe('Dropdown options', () => { + beforeEach(() => { + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + dispatch = jest.spyOn(store, 'dispatch').mockImplementation(() => {}); + }); + + afterEach(() => { + unmockTracking(); + }); + + it.each` + dropdownIndex | dropdownLabel | startActionCalledTimes | trackLabel + ${0} | ${BoardCardMoveToPosition.i18n.moveToStartText} | ${0} | ${'move_to_start'} + ${1} | ${BoardCardMoveToPosition.i18n.moveToEndText} | ${1} | ${'move_to_end'} + `( + 'on click of dropdown index $dropdownIndex with label $dropdownLabel should call moveItem action with tracking label $trackLabel', + async ({ dropdownIndex, startActionCalledTimes, dropdownLabel, trackLabel }) => { + await findEllipsesButton().vm.$emit('click'); + + expect(findDropdownItemAtIndex(dropdownIndex).text()).toBe(dropdownLabel); + await findDropdownItemAtIndex(dropdownIndex).vm.$emit('click', { + stopPropagation: () => {}, + }); + + await nextTick(); + + expect(trackingSpy).toHaveBeenCalledWith('boards:list', 'click_toggle_button', { + category: 'boards:list', + label: trackLabel, + property: 'type_card', + }); + expect(dispatch).toHaveBeenCalledTimes(startActionCalledTimes); + if (startActionCalledTimes) { + expect(dispatch).toHaveBeenCalledWith('moveItem', { + fromListId: mockList.id, + itemId: mockIssue2.id, + itemIid: mockIssue2.iid, + itemPath: mockIssue2.referencePath, + moveBeforeId: undefined, + moveAfterId: undefined, + toListId: mockList.id, + }); + } + }, + ); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index bb1e63a581e..2feaa5dff8c 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -1,5 +1,5 @@ import { GlLabel } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; @@ -45,7 +45,10 @@ describe('Board card', () => { item = mockIssue, } = {}) => { wrapper = mountFn(BoardCard, { - stubs, + stubs: { + ...stubs, + BoardCardInner, + }, store, propsData: { list: mockLabelList, @@ -86,7 +89,7 @@ describe('Board card', () => { describe('when GlLabel is clicked in BoardCardInner', () => { it('doesnt call toggleBoardItem', () => { createStore({ initialState: { isShowingLabels: true } }); - mountComponent({ mountFn: mount, stubs: {} }); + mountComponent(); wrapper.findComponent(GlLabel).trigger('mouseup'); diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb index 13b7a22e791..5dd5ad117b0 100644 --- a/spec/requests/api/graphql/custom_emoji_query_spec.rb +++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb @@ -35,7 +35,17 @@ RSpec.describe 'getting custom emoji within namespace' do expect(graphql_data['group']['customEmoji']['nodes'].first['name']).to eq(custom_emoji.name) end - it 'returns nil when unauthorised' do + it 'returns nil custom emoji when the custom_emoji feature flag is disabled' do + stub_feature_flags(custom_emoji: false) + + post_graphql(custom_emoji_query(group), current_user: current_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(graphql_data['group']).to be_present + expect(graphql_data['group']['customEmoji']).to be_nil + end + + it 'returns nil group when unauthorised' do user = create(:user) post_graphql(custom_emoji_query(group), current_user: user) diff --git a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb index c91437fa355..66facdebe78 100644 --- a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb +++ b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb @@ -39,5 +39,19 @@ RSpec.describe 'Creation of a new Custom Emoji' do expect(gql_response['customEmoji']['name']).to eq(attributes[:name]) expect(gql_response['customEmoji']['url']).to eq(attributes[:url]) end + + context 'when the custom_emoji feature flag is disabled' do + before do + stub_feature_flags(custom_emoji: false) + end + + it 'does nothing and returns and error' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to not_change(CustomEmoji, :count) + + expect_graphql_errors_to_include('Custom emoji feature is disabled') + end + end end end diff --git a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb index 07fd57a2cee..7d25206e617 100644 --- a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb @@ -68,6 +68,20 @@ RSpec.describe 'Deletion of custom emoji' do end it_behaves_like 'deletes custom emoji' + + context 'when the custom_emoji feature flag is disabled' do + before do + stub_feature_flags(custom_emoji: false) + end + + it_behaves_like 'does not delete custom emoji' + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect_graphql_errors_to_include('Custom emoji feature is disabled') + end + end end end end |