Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
fc68a997ca
commit
224076f495
|
|
@ -0,0 +1,195 @@
|
|||
<script>
|
||||
import { GlCollapsibleListbox, GlFormGroup } from '@gitlab/ui';
|
||||
import { debounce, unionBy } from 'lodash';
|
||||
import { createAlert } from '~/alert';
|
||||
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
|
||||
import usersSearchQuery from '~/graphql_shared/queries/workspace_autocomplete_users.query.graphql';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { __ } from '~/locale';
|
||||
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
|
||||
import { BULK_UPDATE_UNASSIGNED } from '../../constants';
|
||||
import { formatUserForListbox } from '../../utils';
|
||||
|
||||
export default {
|
||||
BULK_UPDATE_UNASSIGNED,
|
||||
components: {
|
||||
GlCollapsibleListbox,
|
||||
GlFormGroup,
|
||||
SidebarParticipant,
|
||||
},
|
||||
props: {
|
||||
fullPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isGroup: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentUser: undefined,
|
||||
searchStarted: false,
|
||||
searchTerm: '',
|
||||
selectedId: this.value,
|
||||
users: [],
|
||||
usersCache: [],
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
currentUser: {
|
||||
query: currentUserQuery,
|
||||
},
|
||||
users: {
|
||||
query: usersSearchQuery,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
isProject: !this.isGroup,
|
||||
search: this.searchTerm,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.searchStarted;
|
||||
},
|
||||
update(data) {
|
||||
return this.isGroup ? data.groupWorkspace?.users : data.workspace?.users ?? [];
|
||||
},
|
||||
error(error) {
|
||||
createAlert({
|
||||
message: __('Failed to load assignees. Please try again.'),
|
||||
captureError: true,
|
||||
error,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isLoading() {
|
||||
return this.$apollo.queries.users.loading;
|
||||
},
|
||||
listboxItems() {
|
||||
const listboxItems = [];
|
||||
|
||||
if (!this.searchTerm.trim().length) {
|
||||
listboxItems.push({
|
||||
text: __('Unassigned'),
|
||||
textSrOnly: true,
|
||||
options: [{ text: __('Unassigned'), value: BULK_UPDATE_UNASSIGNED }],
|
||||
});
|
||||
}
|
||||
|
||||
if (this.selectedAssignee) {
|
||||
listboxItems.push({
|
||||
text: __('Selected'),
|
||||
options: [this.selectedAssignee].map(formatUserForListbox),
|
||||
});
|
||||
}
|
||||
|
||||
listboxItems.push({
|
||||
text: __('All'),
|
||||
textSrOnly: true,
|
||||
options: this.users
|
||||
.reduce((acc, user) => {
|
||||
// If user is the selected user, take them out of the list
|
||||
if (user.id === this.selectedId) {
|
||||
return acc;
|
||||
}
|
||||
// If user is the current user, move them to the beginning of the list
|
||||
if (user.id === this.currentUser?.id) {
|
||||
return [user].concat(acc);
|
||||
}
|
||||
return acc.concat(user);
|
||||
}, [])
|
||||
.map(formatUserForListbox),
|
||||
});
|
||||
|
||||
return listboxItems;
|
||||
},
|
||||
selectedAssignee() {
|
||||
return this.usersCache.find((user) => this.selectedId === user.id);
|
||||
},
|
||||
toggleText() {
|
||||
if (this.selectedAssignee) {
|
||||
return this.selectedAssignee.name;
|
||||
}
|
||||
if (this.selectedId === BULK_UPDATE_UNASSIGNED) {
|
||||
return __('Unassigned');
|
||||
}
|
||||
return __('Select assignee');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentUser(currentUser) {
|
||||
this.updateUsersCache([currentUser]);
|
||||
},
|
||||
users(users) {
|
||||
this.updateUsersCache(users);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.setSearchTermDebounced = debounce(this.setSearchTerm, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
},
|
||||
methods: {
|
||||
clearSearch() {
|
||||
this.searchTerm = '';
|
||||
this.$refs.listbox.$refs.searchBox.clearInput?.();
|
||||
},
|
||||
handleSelect(item) {
|
||||
this.selectedId = item;
|
||||
this.$emit('input', item);
|
||||
this.clearSearch();
|
||||
},
|
||||
handleShown() {
|
||||
this.searchTerm = '';
|
||||
this.searchStarted = true;
|
||||
},
|
||||
reset() {
|
||||
this.handleSelect(undefined);
|
||||
this.$refs.listbox.close();
|
||||
},
|
||||
setSearchTerm(searchTerm) {
|
||||
this.searchTerm = searchTerm;
|
||||
},
|
||||
updateUsersCache(users) {
|
||||
// Need to store all users we encounter so we can show "Selected" users
|
||||
// even if they're not found in the apollo `users` list
|
||||
this.usersCache = unionBy(this.usersCache, users, 'id');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form-group :label="__('Assignee')">
|
||||
<gl-collapsible-listbox
|
||||
ref="listbox"
|
||||
block
|
||||
:header-text="__('Select assignee')"
|
||||
is-check-centered
|
||||
:items="listboxItems"
|
||||
:no-results-text="s__('WorkItem|No matching results')"
|
||||
:reset-button-label="__('Reset')"
|
||||
searchable
|
||||
:searching="isLoading"
|
||||
:selected="selectedId"
|
||||
:toggle-text="toggleText"
|
||||
@reset="reset"
|
||||
@search="setSearchTermDebounced"
|
||||
@select="handleSelect"
|
||||
@shown="handleShown"
|
||||
>
|
||||
<template #list-item="{ item }">
|
||||
<template v-if="item.value === $options.BULK_UPDATE_UNASSIGNED">{{ item.text }}</template>
|
||||
<sidebar-participant v-else-if="item" :user="item" />
|
||||
</template>
|
||||
</gl-collapsible-listbox>
|
||||
</gl-form-group>
|
||||
</template>
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
import { GlButton, GlCollapsibleListbox, GlFormGroup } from '@gitlab/ui';
|
||||
import { debounce, intersectionBy, unionBy } from 'lodash';
|
||||
import { createAlert } from '~/alert';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { __, createListFormat, s__, sprintf } from '~/locale';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import groupLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql';
|
||||
import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
|
||||
import { findLabelsWidget, formatLabelForListbox } from '../../utils';
|
||||
|
|
@ -67,11 +67,11 @@ export default {
|
|||
return data.workspace?.labels?.nodes ?? [];
|
||||
},
|
||||
error(error) {
|
||||
this.$emit(
|
||||
'error',
|
||||
s__('WorkItem|Something went wrong when fetching labels. Please try again.'),
|
||||
);
|
||||
Sentry.captureException(error);
|
||||
createAlert({
|
||||
message: s__('WorkItem|Something went wrong when fetching labels. Please try again.'),
|
||||
captureError: true,
|
||||
error,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -132,9 +132,6 @@ export default {
|
|||
searchLabels(searchLabels) {
|
||||
this.updateLabelsCache(searchLabels);
|
||||
},
|
||||
selectedLabelsIds(selectedLabelsIds) {
|
||||
this.selectedIds = selectedLabelsIds;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.setSearchTermDebounced = debounce(this.setSearchTerm, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
|
|
@ -146,8 +143,8 @@ export default {
|
|||
},
|
||||
handleSelect(items) {
|
||||
this.selectedIds = items;
|
||||
this.$emit('select', items);
|
||||
this.clearSearch();
|
||||
this.$emit('select', this.selectedIds);
|
||||
},
|
||||
handleShown() {
|
||||
this.searchTerm = '';
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@ import { createAlert } from '~/alert';
|
|||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { s__ } from '~/locale';
|
||||
import { BULK_UPDATE_UNASSIGNED } from '../../constants';
|
||||
import workItemBulkUpdateMutation from '../../graphql/list/work_item_bulk_update.mutation.graphql';
|
||||
import workItemParent from '../../graphql/list/work_item_parent.query.graphql';
|
||||
import WorkItemBulkEditAssignee from './work_item_bulk_edit_assignee.vue';
|
||||
import WorkItemBulkEditLabels from './work_item_bulk_edit_labels.vue';
|
||||
import WorkItemBulkEditState from './work_item_bulk_edit_state.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlForm,
|
||||
WorkItemBulkEditAssignee,
|
||||
WorkItemBulkEditLabels,
|
||||
WorkItemBulkEditState,
|
||||
},
|
||||
|
|
@ -38,6 +41,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
addLabelIds: [],
|
||||
assigneeId: undefined,
|
||||
parentId: undefined,
|
||||
removeLabelIds: [],
|
||||
state: undefined,
|
||||
|
|
@ -75,7 +79,6 @@ export default {
|
|||
try {
|
||||
await executeBulkEdit();
|
||||
this.$emit('success', { refetchCounts: Boolean(this.state) });
|
||||
this.resetData();
|
||||
} catch (error) {
|
||||
createAlert({
|
||||
message: s__('WorkItem|Something went wrong while bulk editing.'),
|
||||
|
|
@ -102,19 +105,23 @@ export default {
|
|||
});
|
||||
},
|
||||
performLegacyBulkEdit() {
|
||||
let assigneeIds;
|
||||
if (this.assigneeId === BULK_UPDATE_UNASSIGNED) {
|
||||
assigneeIds = [0];
|
||||
} else if (this.assigneeId) {
|
||||
assigneeIds = [getIdFromGraphQLId(this.assigneeId)];
|
||||
}
|
||||
|
||||
const update = {
|
||||
add_label_ids: this.addLabelIds.map(getIdFromGraphQLId),
|
||||
assignee_ids: assigneeIds,
|
||||
issuable_ids: this.checkedItems.map((item) => getIdFromGraphQLId(item.id)).join(','),
|
||||
remove_label_ids: this.removeLabelIds.map(getIdFromGraphQLId),
|
||||
state_event: this.state,
|
||||
};
|
||||
|
||||
return axios.post(this.legacyBulkEditEndpoint, { update });
|
||||
},
|
||||
resetData() {
|
||||
this.addLabelIds = [];
|
||||
this.removeLabelIds = [];
|
||||
this.state = undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -122,6 +129,12 @@ export default {
|
|||
<template>
|
||||
<gl-form id="work-item-list-bulk-edit" class="gl-p-5" @submit.prevent="handleFormSubmitted">
|
||||
<work-item-bulk-edit-state v-if="!isEpicsList" v-model="state" />
|
||||
<work-item-bulk-edit-assignee
|
||||
v-if="!isEpicsList"
|
||||
v-model="assigneeId"
|
||||
:full-path="fullPath"
|
||||
:is-group="isGroup"
|
||||
/>
|
||||
<work-item-bulk-edit-labels
|
||||
:form-label="__('Add labels')"
|
||||
:full-path="fullPath"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { __, s__, sprintf } from '~/locale';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
|
||||
export const BULK_UPDATE_UNASSIGNED = 'unassigned';
|
||||
|
||||
export const STATE_OPEN = 'OPEN';
|
||||
export const STATE_CLOSED = 'CLOSED';
|
||||
|
||||
|
|
|
|||
|
|
@ -120,11 +120,17 @@ export const findHierarchyWidgetAncestors = (workItem) =>
|
|||
findHierarchyWidget(workItem)?.ancestors?.nodes || [];
|
||||
|
||||
export const formatLabelForListbox = (label) => ({
|
||||
text: label.title || label.text,
|
||||
value: label.id || label.value,
|
||||
text: label.title,
|
||||
value: label.id,
|
||||
color: label.color,
|
||||
});
|
||||
|
||||
export const formatUserForListbox = (user) => ({
|
||||
...user,
|
||||
text: user.name,
|
||||
value: user.id,
|
||||
});
|
||||
|
||||
export const convertTypeEnumToName = (workItemTypeEnum) =>
|
||||
Object.keys(NAME_TO_ENUM_MAP).find((name) => NAME_TO_ENUM_MAP[name] === workItemTypeEnum);
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ module UserSettings
|
|||
:location,
|
||||
:mastodon,
|
||||
:name,
|
||||
:orcid,
|
||||
:organization,
|
||||
:private_profile,
|
||||
:pronouns,
|
||||
|
|
|
|||
|
|
@ -396,6 +396,12 @@ module ApplicationHelper
|
|||
external_redirect_path(url: "https://bsky.app/profile/#{user.bluesky}")
|
||||
end
|
||||
|
||||
def orcid_url(user)
|
||||
return '' if user.orcid.blank?
|
||||
|
||||
external_redirect_path(url: "https://orcid.org/#{user.orcid}")
|
||||
end
|
||||
|
||||
def mastodon_url(user)
|
||||
return '' if user.mastodon.blank?
|
||||
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ module UsersHelper
|
|||
end
|
||||
|
||||
def has_contact_info?(user)
|
||||
contact_fields = %i[bluesky discord linkedin mastodon skype twitter website_url github]
|
||||
contact_fields = %i[bluesky discord linkedin mastodon orcid skype twitter website_url github]
|
||||
has_contact = contact_fields.any? { |field| user.public_send(field).present? } # rubocop:disable GitlabSecurity/PublicSend -- fields are controlled, it is safe.
|
||||
has_contact || display_public_email?(user)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -160,6 +160,9 @@
|
|||
%li.form-group.gl-form-group
|
||||
= f.label :github
|
||||
= f.text_field :github, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|username")
|
||||
%li.form-group.gl-form-group
|
||||
= f.label :orcid, s_('Profiles|ORCID')
|
||||
= f.text_field :orcid, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: "1234-1234-1234-1234"
|
||||
.gl-pt-6
|
||||
%fieldset.form-group.gl-form-group
|
||||
%legend.col-form-label
|
||||
|
|
|
|||
|
|
@ -87,3 +87,7 @@
|
|||
.gl-flex.gl-gap-2.gl-mb-2
|
||||
= sprite_icon('github', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
|
||||
= link_to @user.github, github_url(@user), class: 'gl-text-default', title: "GitHub", target: '_blank', rel: 'noopener noreferrer nofollow'
|
||||
- if @user.orcid.present?
|
||||
.gl-flex.gl-gap-2.gl-mb-2
|
||||
= sprite_icon('link', css_class: 'gl-fill-icon-subtle gl-mt-1 flex-shrink-0')
|
||||
= link_to @user.orcid, orcid_url(@user), class: 'gl-text-default', title: "Orcid", target: '_blank', rel: 'noopener noreferrer nofollow'
|
||||
|
|
|
|||
|
|
@ -110,14 +110,14 @@ You might find Agentic Chat particularly helpful when you:
|
|||
|
||||
Agentic Chat works best with natural language questions. Here are some examples:
|
||||
|
||||
- "Read the project structure and explain it to me", or "Explain the project".
|
||||
- "Find the API endpoints that handle user authentication in this codebase."
|
||||
- "Please explain the authorization flow for `<application name>`"
|
||||
- "How do I add a GraphQL mutation in this repository?"
|
||||
- "Show me how error handling is implemented across our application."
|
||||
- "Component `<component name>` has methods for `<x>` and `<y>`. Could you split it up into two components?"
|
||||
- "Could you add in-line documentation for all Java files in `<directory>`?
|
||||
- "Do merge request `<MR URL>` and merge request `<MR URL>` fully address this issue `<issue URL>`?"
|
||||
- `Read the project structure and explain it to me`, or `Explain the project`.
|
||||
- `Find the API endpoints that handle user authentication in this codebase`.
|
||||
- `Please explain the authorization flow for <application name>`.
|
||||
- `How do I add a GraphQL mutation in this repository?`
|
||||
- `Show me how error handling is implemented across our application`.
|
||||
- `Component <component name> has methods for <x> and <y>. Could you split it up into two components?`
|
||||
- `Could you add in-line documentation for all Java files in <directory>?`
|
||||
- `Do merge request <MR URL> and merge request <MR URL> fully address this issue <issue URL>?`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ To add links to other accounts:
|
|||
- Mastodon handle. In GitLab 17.4 and later, you can use your [GitLab profile](#access-your-user-profile) to verify your Mastodon account.
|
||||
- Skype username.
|
||||
- X (formerly Twitter) @username.
|
||||
- [ORCID](https://orcid.org/).
|
||||
|
||||
Your user ID or username must be 500 characters or less.
|
||||
1. Select **Update profile settings**.
|
||||
|
|
|
|||
|
|
@ -47248,6 +47248,9 @@ msgstr ""
|
|||
msgid "Profiles|No file chosen."
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|ORCID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Optional but recommended. If set, key becomes invalid on the specified date."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -159,5 +159,15 @@ RSpec.describe UserSettings::ProfilesController, :request_store, feature_categor
|
|||
expect(response).to have_gitlab_http_status(:found)
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows updating user specified ORCID ID', :aggregate_failures do
|
||||
orcid_id = '1234-1234-1234-1234'
|
||||
sign_in(user)
|
||||
|
||||
put :update, params: { user: { orcid: orcid_id } }
|
||||
|
||||
expect(user.reload.orcid).to eq(orcid_id)
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,217 @@
|
|||
import { GlCollapsibleListbox, GlFormGroup } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import {
|
||||
currentUserResponse,
|
||||
projectMembersAutocompleteResponseWithCurrentUser,
|
||||
} from 'jest/work_items/mock_data';
|
||||
import { createAlert } from '~/alert';
|
||||
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
|
||||
import usersSearchQuery from '~/graphql_shared/queries/workspace_autocomplete_users.query.graphql';
|
||||
import WorkItemBulkEditAssignee from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_assignee.vue';
|
||||
import { BULK_UPDATE_UNASSIGNED } from '~/work_items/constants';
|
||||
|
||||
jest.mock('~/alert');
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('WorkItemBulkEditAssignee component', () => {
|
||||
let wrapper;
|
||||
|
||||
const usersSearchQueryHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(projectMembersAutocompleteResponseWithCurrentUser);
|
||||
const currentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse);
|
||||
|
||||
const createComponent = ({ props = {}, searchQueryHandler = usersSearchQueryHandler } = {}) => {
|
||||
wrapper = mount(WorkItemBulkEditAssignee, {
|
||||
apolloProvider: createMockApollo([
|
||||
[usersSearchQuery, searchQueryHandler],
|
||||
[currentUserQuery, currentUserQueryHandler],
|
||||
]),
|
||||
propsData: {
|
||||
fullPath: 'group/project',
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
GlCollapsibleListbox,
|
||||
GlFormGroup: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
|
||||
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
|
||||
|
||||
const openListboxAndSelect = async (value) => {
|
||||
findListbox().vm.$emit('shown');
|
||||
findListbox().vm.$emit('select', value);
|
||||
await waitForPromises();
|
||||
};
|
||||
|
||||
it('renders the form group', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findFormGroup().attributes('label')).toBe('Assignee');
|
||||
});
|
||||
|
||||
it('renders a header and reset button', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findListbox().props()).toMatchObject({
|
||||
headerText: 'Select assignee',
|
||||
resetButtonLabel: 'Reset',
|
||||
});
|
||||
});
|
||||
|
||||
it('resets the selected assignee when the Reset button is clicked', async () => {
|
||||
createComponent();
|
||||
|
||||
await openListboxAndSelect('gid://gitlab/User/5');
|
||||
|
||||
expect(findListbox().props('selected')).toBe('gid://gitlab/User/5');
|
||||
|
||||
findListbox().vm.$emit('reset');
|
||||
await nextTick();
|
||||
|
||||
expect(findListbox().props('selected')).toEqual([]);
|
||||
});
|
||||
|
||||
describe('users query', () => {
|
||||
it('is not called before dropdown is shown', () => {
|
||||
createComponent();
|
||||
|
||||
expect(usersSearchQueryHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('is called when dropdown is shown', async () => {
|
||||
createComponent();
|
||||
|
||||
findListbox().vm.$emit('shown');
|
||||
await nextTick();
|
||||
|
||||
expect(usersSearchQueryHandler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits an error when there is an error in the call', async () => {
|
||||
createComponent({ searchQueryHandler: jest.fn().mockRejectedValue(new Error('error!')) });
|
||||
|
||||
findListbox().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
captureError: true,
|
||||
error: new Error('error!'),
|
||||
message: 'Failed to load assignees. Please try again.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listbox items', () => {
|
||||
describe('with no selected user', () => {
|
||||
it('renders all users with current user Administrator at the top', async () => {
|
||||
createComponent();
|
||||
|
||||
findListbox().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findListbox().props('items')).toEqual([
|
||||
{
|
||||
text: 'Unassigned',
|
||||
textSrOnly: true,
|
||||
options: [{ text: 'Unassigned', value: BULK_UPDATE_UNASSIGNED }],
|
||||
},
|
||||
{
|
||||
text: 'All',
|
||||
textSrOnly: true,
|
||||
options: [
|
||||
expect.objectContaining({ text: 'Administrator', value: 'gid://gitlab/User/1' }),
|
||||
expect.objectContaining({ text: 'rookie', value: 'gid://gitlab/User/5' }),
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with selected user', () => {
|
||||
it('renders a "Selected" group and an "All" group', async () => {
|
||||
createComponent();
|
||||
|
||||
await openListboxAndSelect('gid://gitlab/User/5');
|
||||
|
||||
expect(findListbox().props('items')).toEqual([
|
||||
{
|
||||
text: 'Unassigned',
|
||||
textSrOnly: true,
|
||||
options: [{ text: 'Unassigned', value: BULK_UPDATE_UNASSIGNED }],
|
||||
},
|
||||
{
|
||||
text: 'Selected',
|
||||
options: [expect.objectContaining({ text: 'rookie', value: 'gid://gitlab/User/5' })],
|
||||
},
|
||||
{
|
||||
text: 'All',
|
||||
textSrOnly: true,
|
||||
options: [
|
||||
expect.objectContaining({ text: 'Administrator', value: 'gid://gitlab/User/1' }),
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with search', () => {
|
||||
it('does not show "Unassigned"', async () => {
|
||||
createComponent();
|
||||
|
||||
findListbox().vm.$emit('shown');
|
||||
findListbox().vm.$emit('search', 'Admin');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findListbox().props('items')).toEqual([
|
||||
{
|
||||
text: 'All',
|
||||
textSrOnly: true,
|
||||
options: [
|
||||
expect.objectContaining({ text: 'Administrator', value: 'gid://gitlab/User/1' }),
|
||||
expect.objectContaining({ text: 'rookie', value: 'gid://gitlab/User/5' }),
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listbox text', () => {
|
||||
describe('with no selected user', () => {
|
||||
it('renders "Select assignee"', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findListbox().props('toggleText')).toBe('Select assignee');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with selected user', () => {
|
||||
it('renders "rookie"', async () => {
|
||||
createComponent();
|
||||
|
||||
await openListboxAndSelect('gid://gitlab/User/5');
|
||||
|
||||
expect(findListbox().props('toggleText')).toBe('rookie');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with unassigned', () => {
|
||||
it('renders "Unassigned"', async () => {
|
||||
createComponent();
|
||||
|
||||
await openListboxAndSelect('unassigned');
|
||||
|
||||
expect(findListbox().props('toggleText')).toBe('Unassigned');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -6,12 +6,12 @@ import VueApollo from 'vue-apollo';
|
|||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { projectLabelsResponse } from 'jest/work_items/mock_data';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { createAlert } from '~/alert';
|
||||
import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
|
||||
import WorkItemBulkEditLabels from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_labels.vue';
|
||||
import { WIDGET_TYPE_LABELS } from '~/work_items/constants';
|
||||
|
||||
jest.mock('~/sentry/sentry_browser_wrapper');
|
||||
jest.mock('~/alert');
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
|
|
@ -112,10 +112,11 @@ describe('WorkItemBulkEditLabels component', () => {
|
|||
findListbox().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([
|
||||
['Something went wrong when fetching labels. Please try again.'],
|
||||
]);
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('error!'));
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
captureError: true,
|
||||
error: new Error('error!'),
|
||||
message: 'Something went wrong when fetching labels. Please try again.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import { createAlert } from '~/alert';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import WorkItemBulkEditAssignee from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_assignee.vue';
|
||||
import WorkItemBulkEditLabels from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_labels.vue';
|
||||
import WorkItemBulkEditSidebar from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar.vue';
|
||||
import WorkItemBulkEditState from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_state.vue';
|
||||
|
|
@ -50,6 +51,7 @@ describe('WorkItemBulkEditSidebar component', () => {
|
|||
|
||||
const findForm = () => wrapper.findComponent(GlForm);
|
||||
const findStateComponent = () => wrapper.findComponent(WorkItemBulkEditState);
|
||||
const findAssigneeComponent = () => wrapper.findComponent(WorkItemBulkEditAssignee);
|
||||
const findAddLabelsComponent = () => wrapper.findAllComponents(WorkItemBulkEditLabels).at(0);
|
||||
const findRemoveLabelsComponent = () => wrapper.findAllComponents(WorkItemBulkEditLabels).at(1);
|
||||
|
||||
|
|
@ -114,14 +116,24 @@ describe('WorkItemBulkEditSidebar component', () => {
|
|||
it('makes POST request to bulk edit', async () => {
|
||||
const issuable_ids = '11,22'; // eslint-disable-line camelcase
|
||||
const add_label_ids = [1, 2, 3]; // eslint-disable-line camelcase
|
||||
const assignee_ids = [5]; // eslint-disable-line camelcase
|
||||
const remove_label_ids = [4, 5, 6]; // eslint-disable-line camelcase
|
||||
const state_event = 'reopen'; // eslint-disable-line camelcase
|
||||
axiosMock.onPost().replyOnce(HTTP_STATUS_OK);
|
||||
createComponent({ props: { isEpicsList: false } });
|
||||
|
||||
findStateComponent().vm.$emit('input', state_event);
|
||||
findAddLabelsComponent().vm.$emit('select', add_label_ids);
|
||||
findRemoveLabelsComponent().vm.$emit('select', remove_label_ids);
|
||||
findAssigneeComponent().vm.$emit('input', 'gid://gitlab/User/5');
|
||||
findAddLabelsComponent().vm.$emit('select', [
|
||||
'gid://gitlab/Label/1',
|
||||
'gid://gitlab/Label/2',
|
||||
'gid://gitlab/Label/3',
|
||||
]);
|
||||
findRemoveLabelsComponent().vm.$emit('select', [
|
||||
'gid://gitlab/Label/4',
|
||||
'gid://gitlab/Label/5',
|
||||
'gid://gitlab/Label/6',
|
||||
]);
|
||||
findForm().vm.$emit('submit', { preventDefault: () => {} });
|
||||
await waitForPromises();
|
||||
|
||||
|
|
@ -130,15 +142,13 @@ describe('WorkItemBulkEditSidebar component', () => {
|
|||
JSON.stringify({
|
||||
update: {
|
||||
add_label_ids,
|
||||
assignee_ids,
|
||||
issuable_ids,
|
||||
remove_label_ids,
|
||||
state_event,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(findStateComponent().props('value')).toBeUndefined();
|
||||
expect(findAddLabelsComponent().props('selectedLabelsIds')).toEqual([]);
|
||||
expect(findRemoveLabelsComponent().props('selectedLabelsIds')).toEqual([]);
|
||||
});
|
||||
|
||||
it('renders error when there is a response error', async () => {
|
||||
|
|
@ -188,6 +198,23 @@ describe('WorkItemBulkEditSidebar component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('"Assignee" component', () => {
|
||||
it.each([true, false])('renders depending on isEpicsList prop', (isEpicsList) => {
|
||||
createComponent({ props: { isEpicsList } });
|
||||
|
||||
expect(findAssigneeComponent().exists()).toBe(!isEpicsList);
|
||||
});
|
||||
|
||||
it('updates assignee when "Assignee" component emits "input" event', async () => {
|
||||
createComponent();
|
||||
|
||||
findAssigneeComponent().vm.$emit('input', 'gid://gitlab/User/5');
|
||||
await nextTick();
|
||||
|
||||
expect(findAssigneeComponent().props('value')).toBe('gid://gitlab/User/5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('"Add labels" component', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import {
|
|||
import {
|
||||
autocompleteDataSources,
|
||||
convertTypeEnumToName,
|
||||
formatLabelForListbox,
|
||||
formatUserForListbox,
|
||||
markdownPreviewPath,
|
||||
newWorkItemPath,
|
||||
isReference,
|
||||
|
|
@ -42,6 +44,51 @@ import {
|
|||
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
||||
import { TYPE_EPIC } from '~/issues/constants';
|
||||
|
||||
describe('formatLabelForListbox', () => {
|
||||
const label = {
|
||||
__typename: 'Label',
|
||||
id: 'gid://gitlab/Label/1',
|
||||
title: 'Label 1',
|
||||
description: '',
|
||||
color: '#f00',
|
||||
textColor: '#00f',
|
||||
};
|
||||
|
||||
it('formats as expected', () => {
|
||||
expect(formatLabelForListbox(label)).toEqual({
|
||||
text: 'Label 1',
|
||||
value: 'gid://gitlab/Label/1',
|
||||
color: '#f00',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatUserForListbox', () => {
|
||||
const user = {
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl: '',
|
||||
webUrl: '',
|
||||
webPath: '/doe_I',
|
||||
name: 'John Doe',
|
||||
username: 'doe_I',
|
||||
};
|
||||
|
||||
it('formats as expected', () => {
|
||||
expect(formatUserForListbox(user)).toEqual({
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl: '',
|
||||
webUrl: '',
|
||||
webPath: '/doe_I',
|
||||
name: 'John Doe',
|
||||
username: 'doe_I',
|
||||
text: 'John Doe',
|
||||
value: 'gid://gitlab/User/1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('autocompleteDataSources', () => {
|
||||
beforeEach(() => {
|
||||
gon.relative_url_root = '/foobar';
|
||||
|
|
|
|||
|
|
@ -697,6 +697,26 @@ RSpec.describe ApplicationHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#orcid_url' do
|
||||
let(:user) { build(:user) }
|
||||
|
||||
subject(:orcid) { orcid_url(user) }
|
||||
|
||||
context 'without ORCID ID' do
|
||||
it 'returns an empty string' do
|
||||
expect(orcid).to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with ORCID ID' do
|
||||
it 'returns orcid url' do
|
||||
user.orcid = '1234-1234-1234-1234'
|
||||
|
||||
expect(orcid).to eq(external_redirect_path(url: 'https://orcid.org/1234-1234-1234-1234'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#gitlab_ui_form_for' do
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ RSpec.describe UsersHelper, feature_category: :user_management do
|
|||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'when user has ORCID' do
|
||||
let_it_be(:user) { create(:user, orcid: '1234-1234-1234-1234') }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'when user has public email' do
|
||||
let_it_be(:user) { create(:user, :public_email) }
|
||||
|
||||
|
|
@ -32,6 +38,12 @@ RSpec.describe UsersHelper, feature_category: :user_management do
|
|||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'when user ORCID is blank' do
|
||||
let_it_be(:user) { create(:user, orcid: '') }
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'display_public_email?' do
|
||||
|
|
|
|||
Loading…
Reference in New Issue