Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-18 06:09:51 +00:00
parent f3f9b9fe66
commit ad3b511ba3
21 changed files with 384 additions and 13 deletions

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -146,3 +146,5 @@ export default {
BoardType,
ListType,
};
export const DEFAULT_BOARD_LIST_ITEMS_SIZE = 10;

View File

@ -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,
};

View File

@ -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);

View File

@ -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

View File

@ -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!

View File

@ -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

View File

@ -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

View File

@ -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. |

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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}`);
});

View File

@ -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,

View File

@ -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,
});
}
},
);
});
});
});

View File

@ -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');

View File

@ -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)

View File

@ -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

View File

@ -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