diff --git a/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_assignee.vue b/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_assignee.vue
new file mode 100644
index 00000000000..c7588d77c75
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_assignee.vue
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+ {{ item.text }}
+
+
+
+
+
diff --git a/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_labels.vue b/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_labels.vue
index 8daa06e1a4d..a2aaa542e6d 100644
--- a/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_labels.vue
@@ -1,9 +1,9 @@
@@ -122,6 +129,12 @@ export default {
+
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);
diff --git a/app/controllers/user_settings/profiles_controller.rb b/app/controllers/user_settings/profiles_controller.rb
index db73623f151..780713432ee 100644
--- a/app/controllers/user_settings/profiles_controller.rb
+++ b/app/controllers/user_settings/profiles_controller.rb
@@ -56,6 +56,7 @@ module UserSettings
:location,
:mastodon,
:name,
+ :orcid,
:organization,
:private_profile,
:pronouns,
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index e407a6963b2..30ac0d9de89 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -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?
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 1f92f80ae44..1395bf0186c 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -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
diff --git a/app/views/user_settings/profiles/show.html.haml b/app/views/user_settings/profiles/show.html.haml
index 59921fbe7d7..a06a3cd26c5 100644
--- a/app/views/user_settings/profiles/show.html.haml
+++ b/app/views/user_settings/profiles/show.html.haml
@@ -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
diff --git a/app/views/users/_profile_sidebar.html.haml b/app/views/users/_profile_sidebar.html.haml
index 13090cdef3b..ff3910b42f0 100644
--- a/app/views/users/_profile_sidebar.html.haml
+++ b/app/views/users/_profile_sidebar.html.haml
@@ -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'
diff --git a/doc/user/gitlab_duo_chat/agentic_chat.md b/doc/user/gitlab_duo_chat/agentic_chat.md
index cc67d2280a9..b1281e27df8 100644
--- a/doc/user/gitlab_duo_chat/agentic_chat.md
+++ b/doc/user/gitlab_duo_chat/agentic_chat.md
@@ -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 ``"
-- "How do I add a GraphQL mutation in this repository?"
-- "Show me how error handling is implemented across our application."
-- "Component `` has methods for `` and ``. Could you split it up into two components?"
-- "Could you add in-line documentation for all Java files in ``?
-- "Do merge request `` and merge request `` fully address this issue ``?"
+- `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 `.
+- `How do I add a GraphQL mutation in this repository?`
+- `Show me how error handling is implemented across our application`.
+- `Component has methods for and . Could you split it up into two components?`
+- `Could you add in-line documentation for all Java files in ?`
+- `Do merge request and merge request fully address this issue ?`
## Troubleshooting
diff --git a/doc/user/profile/_index.md b/doc/user/profile/_index.md
index 0c1a1f1caac..aafcf8342a6 100644
--- a/doc/user/profile/_index.md
+++ b/doc/user/profile/_index.md
@@ -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**.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 473ae46d6f3..b8453fd93b9 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/controllers/user_settings/profiles_controller_spec.rb b/spec/controllers/user_settings/profiles_controller_spec.rb
index fdaef89633b..42c49108b20 100644
--- a/spec/controllers/user_settings/profiles_controller_spec.rb
+++ b/spec/controllers/user_settings/profiles_controller_spec.rb
@@ -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
diff --git a/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_assignee_spec.js b/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_assignee_spec.js
new file mode 100644
index 00000000000..0b582557f58
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_assignee_spec.js
@@ -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');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_labels_spec.js b/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_labels_spec.js
index 6faf6fd0fc3..4040c9551c3 100644
--- a/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_labels_spec.js
@@ -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.',
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar_spec.js b/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar_spec.js
index 5668fdca451..16adeb4025a 100644
--- a/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar_spec.js
+++ b/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar_spec.js
@@ -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();
diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js
index 291425f27e4..9d970302124 100644
--- a/spec/frontend/work_items/utils_spec.js
+++ b/spec/frontend/work_items/utils_spec.js
@@ -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';
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 57c7a1ab2d6..1cfb65bfa0d 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -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
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index ff3a789df85..57fcb4cdcdd 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -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