Commit 30741c7a authored by Miguel Rincon's avatar Miguel Rincon

Merge branch 'jira-issue-entity-add-gitlab-web-url' into 'master'

Add `gitlab_web_url` to Jira issue entity

See merge request gitlab-org/gitlab!53804
parents 14acddd8 72121e71
...@@ -4,6 +4,7 @@ import { GlLink, GlIcon, GlLabel, GlFormCheckbox, GlTooltipDirective } from '@gi ...@@ -4,6 +4,7 @@ import { GlLink, GlIcon, GlLabel, GlFormCheckbox, GlTooltipDirective } from '@gi
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils'; import { isScopedLabel } from '~/lib/utils/common_utils';
import { getTimeago } from '~/lib/utils/datetime_utility'; import { getTimeago } from '~/lib/utils/datetime_utility';
import { isExternal, setUrlFragment } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import IssuableAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; import IssuableAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
...@@ -47,17 +48,14 @@ export default { ...@@ -47,17 +48,14 @@ export default {
author() { author() {
return this.issuable.author; return this.issuable.author;
}, },
webUrl() {
return this.issuable.gitlabWebUrl || this.issuable.webUrl;
},
authorId() { authorId() {
return getIdFromGraphQLId(`${this.author.id}`); return getIdFromGraphQLId(`${this.author.id}`);
}, },
isIssuableUrlExternal() { isIssuableUrlExternal() {
// Check if URL is relative, which means it is internal. return isExternal(this.webUrl);
if (!/^https?:\/\//g.test(this.issuable.webUrl)) {
return false;
}
// In case URL is absolute, it may or may not be internal,
// hence use `gon.gitlab_url` which is current instance domain.
return !this.issuable.webUrl.includes(gon.gitlab_url);
}, },
labels() { labels() {
return this.issuable.labels?.nodes || this.issuable.labels || []; return this.issuable.labels?.nodes || this.issuable.labels || [];
...@@ -91,6 +89,9 @@ export default { ...@@ -91,6 +89,9 @@ export default {
this.hasSlotContents('status') || this.showDiscussions || this.issuable.assignees, this.hasSlotContents('status') || this.showDiscussions || this.issuable.assignees,
); );
}, },
issuableNotesLink() {
return setUrlFragment(this.webUrl, 'notes');
},
}, },
methods: { methods: {
hasSlotContents(slotName) { hasSlotContents(slotName) {
...@@ -144,7 +145,7 @@ export default { ...@@ -144,7 +145,7 @@ export default {
name="eye-slash" name="eye-slash"
:title="__('Confidential')" :title="__('Confidential')"
/> />
<gl-link :href="issuable.webUrl" v-bind="issuableTitleProps" <gl-link :href="webUrl" v-bind="issuableTitleProps"
>{{ issuable.title >{{ issuable.title
}}<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" }}<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2"
/></gl-link> /></gl-link>
...@@ -206,7 +207,7 @@ export default { ...@@ -206,7 +207,7 @@ export default {
<gl-link <gl-link
v-gl-tooltip:tooltipcontainer.top v-gl-tooltip:tooltipcontainer.top
:title="__('Comments')" :title="__('Comments')"
:href="`${issuable.webUrl}#notes`" :href="issuableNotesLink"
:class="{ 'no-comments': !issuable.userDiscussionsCount }" :class="{ 'no-comments': !issuable.userDiscussionsCount }"
class="gl-reset-color!" class="gl-reset-color!"
> >
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Integrations module Integrations
module Jira module Jira
class IssueEntity < Grape::Entity class IssueEntity < Grape::Entity
include RequestAwareEntity
expose :project_id do |_jira_issue, options| expose :project_id do |_jira_issue, options|
options[:project].id options[:project].id
end end
...@@ -61,6 +63,14 @@ module Integrations ...@@ -61,6 +63,14 @@ module Integrations
"#{base_web_url(jira_issue)}/browse/#{jira_issue.key}" "#{base_web_url(jira_issue)}/browse/#{jira_issue.key}"
end end
expose :gitlab_web_url do |jira_issue|
if ::Feature.enabled?(:jira_issues_show_integration, options[:project], default_enabled: :yaml)
project_integrations_jira_issue_path(options[:project], jira_issue.key)
else
nil
end
end
expose :references do |jira_issue| expose :references do |jira_issue|
{ {
relative: jira_issue.key relative: jira_issue.key
......
...@@ -56,6 +56,7 @@ RSpec.describe Integrations::Jira::IssueEntity do ...@@ -56,6 +56,7 @@ RSpec.describe Integrations::Jira::IssueEntity do
{ name: 'assignee' } { name: 'assignee' }
], ],
web_url: 'http://jira.com/browse/GL-5', web_url: 'http://jira.com/browse/GL-5',
gitlab_web_url: Gitlab::Routing.url_helpers.project_integrations_jira_issue_path(project, 'GL-5'),
references: { relative: 'GL-5' }, references: { relative: 'GL-5' },
external_tracker: 'jira' external_tracker: 'jira'
) )
...@@ -109,4 +110,14 @@ RSpec.describe Integrations::Jira::IssueEntity do ...@@ -109,4 +110,14 @@ RSpec.describe Integrations::Jira::IssueEntity do
expect(subject).to include(labels: []) expect(subject).to include(labels: [])
end end
end end
context 'feature flag "jira_issues_show_integration" is disabled' do
before do
stub_feature_flags(jira_issues_show_integration: false)
end
it 'sets `gitlab_web_url` to nil' do
expect(subject[:gitlab_web_url]).to eq(nil)
end
end
end end
...@@ -18,6 +18,8 @@ const createComponent = ({ issuableSymbol = '#', issuable = mockIssuable, slots ...@@ -18,6 +18,8 @@ const createComponent = ({ issuableSymbol = '#', issuable = mockIssuable, slots
slots, slots,
}); });
const MOCK_GITLAB_URL = 'http://0.0.0.0:3000';
describe('IssuableItem', () => { describe('IssuableItem', () => {
// The mock data is dependent that this is after our default date // The mock data is dependent that this is after our default date
useFakeDate(2020, 11, 11); useFakeDate(2020, 11, 11);
...@@ -28,7 +30,7 @@ describe('IssuableItem', () => { ...@@ -28,7 +30,7 @@ describe('IssuableItem', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
gon.gitlab_url = 'http://0.0.0.0:3000'; gon.gitlab_url = MOCK_GITLAB_URL;
wrapper = createComponent(); wrapper = createComponent();
}); });
...@@ -73,11 +75,11 @@ describe('IssuableItem', () => { ...@@ -73,11 +75,11 @@ describe('IssuableItem', () => {
describe('isIssuableUrlExternal', () => { describe('isIssuableUrlExternal', () => {
it.each` it.each`
issuableWebUrl | urlType | returnValue issuableWebUrl | urlType | returnValue
${'/gitlab-org/gitlab-test/-/issues/2'} | ${'relative'} | ${false} ${'/gitlab-org/gitlab-test/-/issues/2'} | ${'relative'} | ${false}
${'http://0.0.0.0:3000/gitlab-org/gitlab-test/-/issues/1'} | ${'absolute and internal'} | ${false} ${`${MOCK_GITLAB_URL}/gitlab-org/gitlab-test/-/issues/1`} | ${'absolute and internal'} | ${false}
${'http://jira.atlassian.net/browse/IG-1'} | ${'external'} | ${true} ${'http://jira.atlassian.net/browse/IG-1'} | ${'external'} | ${true}
${'https://github.com/gitlabhq/gitlabhq/issues/1'} | ${'external'} | ${true} ${'https://github.com/gitlabhq/gitlabhq/issues/1'} | ${'external'} | ${true}
`( `(
'returns $returnValue when `issuable.webUrl` is $urlType', 'returns $returnValue when `issuable.webUrl` is $urlType',
async ({ issuableWebUrl, returnValue }) => { async ({ issuableWebUrl, returnValue }) => {
...@@ -217,14 +219,32 @@ describe('IssuableItem', () => { ...@@ -217,14 +219,32 @@ describe('IssuableItem', () => {
}); });
describe('template', () => { describe('template', () => {
it('renders issuable title', () => { it.each`
const titleEl = wrapper.find('[data-testid="issuable-title"]'); gitlabWebUrl | webUrl | expectedHref | expectedTarget
${undefined} | ${`${MOCK_GITLAB_URL}/issue`} | ${`${MOCK_GITLAB_URL}/issue`} | ${undefined}
${undefined} | ${'https://jira.com/issue'} | ${'https://jira.com/issue'} | ${'_blank'}
${'/gitlab-org/issue'} | ${'https://jira.com/issue'} | ${'/gitlab-org/issue'} | ${undefined}
`(
'renders issuable title correctly when `gitlabWebUrl` is `$gitlabWebUrl` and webUrl is `$webUrl`',
async ({ webUrl, gitlabWebUrl, expectedHref, expectedTarget }) => {
wrapper.setProps({
issuable: {
...mockIssuable,
webUrl,
gitlabWebUrl,
},
});
expect(titleEl.exists()).toBe(true); await wrapper.vm.$nextTick();
expect(titleEl.find(GlLink).attributes('href')).toBe(mockIssuable.webUrl);
expect(titleEl.find(GlLink).attributes('target')).not.toBeDefined(); const titleEl = wrapper.find('[data-testid="issuable-title"]');
expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title);
}); expect(titleEl.exists()).toBe(true);
expect(titleEl.find(GlLink).attributes('href')).toBe(expectedHref);
expect(titleEl.find(GlLink).attributes('target')).toBe(expectedTarget);
expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title);
},
);
it('renders checkbox when `showCheckbox` prop is true', async () => { it('renders checkbox when `showCheckbox` prop is true', async () => {
wrapper.setProps({ wrapper.setProps({
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment