Commit 533ae5a8 authored by Justin Ho Tuan Duong's avatar Justin Ho Tuan Duong Committed by Olena Horal-Koretska

Add basic app structure with modal and button

And import all styles which will be used by child
components
parent 901bb7dd
...@@ -22,3 +22,12 @@ export const removeSubscription = async (removePath) => { ...@@ -22,3 +22,12 @@ export const removeSubscription = async (removePath) => {
}, },
}); });
}; };
export const fetchGroups = async (groupsPath, { page, perPage }) => {
return axios.get(groupsPath, {
params: {
page,
per_page: perPage,
},
});
};
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { GlAlert } from '@gitlab/ui'; import { GlAlert, GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import GroupsList from './groups_list.vue';
export default { export default {
name: 'JiraConnectApp', name: 'JiraConnectApp',
components: { components: {
GlAlert, GlAlert,
GlButton,
GlModal,
GroupsList,
},
directives: {
GlModalDirective,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
computed: { computed: {
...mapState(['errorMessage']), ...mapState(['errorMessage']),
showNewUi() { showNewUI() {
return this.glFeatures.newJiraConnectUi; return this.glFeatures.newJiraConnectUi;
}, },
}, },
modal: {
cancelProps: {
text: __('Cancel'),
},
},
}; };
</script> </script>
...@@ -26,8 +39,25 @@ export default { ...@@ -26,8 +39,25 @@ export default {
<h1>GitLab for Jira Configuration</h1> <h1>GitLab for Jira Configuration</h1>
<div v-if="showNewUi"> <div
v-if="showNewUI"
class="gl-display-flex gl-justify-content-space-between gl-my-5 gl-pb-4 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200"
>
<h3 data-testid="new-jira-connect-ui-heading">{{ s__('Integrations|Linked namespaces') }}</h3> <h3 data-testid="new-jira-connect-ui-heading">{{ s__('Integrations|Linked namespaces') }}</h3>
<gl-button
v-gl-modal-directive="'add-namespace-modal'"
category="primary"
variant="info"
class="gl-align-self-center"
>{{ s__('Integrations|Add namespace') }}</gl-button
>
<gl-modal
modal-id="add-namespace-modal"
:title="s__('Integrations|Link namespaces')"
:action-cancel="$options.modal.cancelProps"
>
<groups-list />
</gl-modal>
</div> </div>
</div> </div>
</template> </template>
<script>
import { GlTabs, GlTab, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { s__ } from '~/locale';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { fetchGroups } from '~/jira_connect/api';
import { defaultPerPage } from '~/jira_connect/constants';
import GroupsListItem from './groups_list_item.vue';
export default {
components: {
GlTabs,
GlTab,
GlLoadingIcon,
GlPagination,
GroupsListItem,
},
inject: {
groupsPath: {
default: '',
},
},
data() {
return {
groups: [],
isLoading: false,
page: 1,
perPage: defaultPerPage,
totalItems: 0,
};
},
mounted() {
this.loadGroups();
},
methods: {
loadGroups() {
this.isLoading = true;
fetchGroups(this.groupsPath, {
page: this.page,
perPage: this.perPage,
})
.then((response) => {
const { page, total } = parseIntPagination(normalizeHeaders(response.headers));
this.page = page;
this.totalItems = total;
this.groups = response.data;
})
.catch(() => {
// eslint-disable-next-line no-alert
alert(s__('Integrations|Failed to load namespaces. Please try again.'));
})
.finally(() => {
this.isLoading = false;
});
},
},
};
</script>
<template>
<gl-tabs>
<gl-tab :title="__('Groups and subgroups')" class="gl-pt-3">
<gl-loading-icon v-if="isLoading" size="md" />
<div v-else-if="groups.length === 0" class="gl-text-center">
<h5>{{ s__('Integrations|No available namespaces.') }}</h5>
<p class="gl-mt-5">
{{
s__('Integrations|You must have owner or maintainer permissions to link namespaces.')
}}
</p>
</div>
<ul v-else class="gl-list-style-none gl-pl-0">
<groups-list-item v-for="group in groups" :key="group.id" :group="group" />
</ul>
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-pagination
v-if="totalItems > perPage && groups.length > 0"
v-model="page"
class="gl-mb-0"
:per-page="perPage"
:total-items="totalItems"
@input="loadGroups"
/>
</div>
</gl-tab>
</gl-tabs>
</template>
<script>
import { GlIcon, GlAvatar } from '@gitlab/ui';
export default {
components: {
GlIcon,
GlAvatar,
},
props: {
group: {
type: Object,
required: true,
},
},
};
</script>
<template>
<li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-200">
<div class="gl-display-flex gl-align-items-center gl-py-3">
<gl-icon name="folder-o" class="gl-mr-3" />
<div class="gl-display-none gl-flex-shrink-0 gl-display-sm-flex gl-mr-3">
<gl-avatar :size="32" shape="rect" :entity-name="group.name" :src="group.avatar_url" />
</div>
<div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center">
<div class="gl-min-w-0 gl-flex-grow-1 flex-shrink-1">
<div class="gl-display-flex gl-align-items-center gl-flex-wrap">
<span
class="gl-mr-3 gl-text-gray-900! gl-font-weight-bold"
data-testid="group-list-item-name"
>
{{ group.full_name }}
</span>
</div>
<div v-if="group.description" data-testid="group-list-item-description">
<p class="gl-mt-2! gl-mb-0 gl-text-gray-600" v-text="group.description"></p>
</div>
</div>
</div>
</div>
</li>
</template>
...@@ -73,11 +73,16 @@ function initJiraConnect() { ...@@ -73,11 +73,16 @@ function initJiraConnect() {
Vue.use(Translate); Vue.use(Translate);
Vue.use(GlFeatureFlagsPlugin); Vue.use(GlFeatureFlagsPlugin);
const { groupsPath } = el.dataset;
return new Vue({ return new Vue({
el, el,
store, store,
provide: {
groupsPath,
},
render(createElement) { render(createElement) {
return createElement(JiraConnectApp, {}); return createElement(JiraConnectApp);
}, },
}); });
} }
......
@import 'mixins_and_variables_and_functions'; @import 'mixins_and_variables_and_functions';
/**
NOTE: We should only import styles that we actually use.
Ex:
@import '@gitlab/ui/src/scss/gitlab_ui';
*/
@import '@gitlab/ui/src/scss/bootstrap'; @import '@gitlab/ui/src/scss/bootstrap';
@import 'bootstrap-vue/src/index'; @import 'bootstrap-vue/src/index';
@import '@gitlab/ui/src/scss/utilities'; @import '@gitlab/ui/src/scss/utilities';
@import '@gitlab/ui/src/components/base/alert/alert'; @import '@gitlab/ui/src/components/base/alert/alert';
// We should only import styles that we actually use.
@import '@gitlab/ui/src/components/base/alert/alert';
@import '@gitlab/ui/src/components/base/avatar/avatar';
@import '@gitlab/ui/src/components/base/badge/badge';
@import '@gitlab/ui/src/components/base/button/button';
@import '@gitlab/ui/src/components/base/icon/icon';
@import '@gitlab/ui/src/components/base/link/link';
@import '@gitlab/ui/src/components/base/loading_icon/loading_icon';
@import '@gitlab/ui/src/components/base/modal/modal';
@import '@gitlab/ui/src/components/base/pagination/pagination';
@import '@gitlab/ui/src/components/base/tabs/tabs/tabs';
@import '@gitlab/ui/src/components/base/tooltip/tooltip';
$atlaskit-border-color: #dfe1e6; $atlaskit-border-color: #dfe1e6;
.ac-content { .ac-content {
......
...@@ -4,4 +4,10 @@ module JiraConnectHelper ...@@ -4,4 +4,10 @@ module JiraConnectHelper
def new_jira_connect_ui? def new_jira_connect_ui?
Feature.enabled?(:new_jira_connect_ui, type: :development, default_enabled: :yaml) Feature.enabled?(:new_jira_connect_ui, type: :development, default_enabled: :yaml)
end end
def jira_connect_app_data
{
groups_path: api_v4_groups_path(params: { min_access_level: Gitlab::Access::MAINTAINER })
}
end
end end
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
.gl-mt-5 .gl-mt-5
%p Note: this integration only works with accounts on GitLab.com (SaaS). %p Note: this integration only works with accounts on GitLab.com (SaaS).
- else - else
.js-jira-connect-app .js-jira-connect-app{ data: jira_connect_app_data }
- unless new_jira_connect_ui? - unless new_jira_connect_ui?
%form#add-subscription-form.subscription-form{ action: jira_connect_subscriptions_path } %form#add-subscription-form.subscription-form{ action: jira_connect_subscriptions_path }
......
...@@ -15323,6 +15323,9 @@ msgstr "" ...@@ -15323,6 +15323,9 @@ msgstr ""
msgid "Integrations|%{integration} settings saved, but not active." msgid "Integrations|%{integration} settings saved, but not active."
msgstr "" msgstr ""
msgid "Integrations|Add namespace"
msgstr ""
msgid "Integrations|All details" msgid "Integrations|All details"
msgstr "" msgstr ""
...@@ -15353,6 +15356,9 @@ msgstr "" ...@@ -15353,6 +15356,9 @@ msgstr ""
msgid "Integrations|Enable comments" msgid "Integrations|Enable comments"
msgstr "" msgstr ""
msgid "Integrations|Failed to load namespaces. Please try again."
msgstr ""
msgid "Integrations|Includes Standard plus entire commit message, commit hash, and issue IDs" msgid "Integrations|Includes Standard plus entire commit message, commit hash, and issue IDs"
msgstr "" msgstr ""
...@@ -15362,12 +15368,18 @@ msgstr "" ...@@ -15362,12 +15368,18 @@ msgstr ""
msgid "Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira." msgid "Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira."
msgstr "" msgstr ""
msgid "Integrations|Link namespaces"
msgstr ""
msgid "Integrations|Linked namespaces" msgid "Integrations|Linked namespaces"
msgstr "" msgstr ""
msgid "Integrations|Namespaces are your GitLab groups and subgroups that will be linked to this Jira instance." msgid "Integrations|Namespaces are your GitLab groups and subgroups that will be linked to this Jira instance."
msgstr "" msgstr ""
msgid "Integrations|No available namespaces."
msgstr ""
msgid "Integrations|Projects using custom settings will not be affected." msgid "Integrations|Projects using custom settings will not be affected."
msgstr "" msgstr ""
...@@ -15419,6 +15431,9 @@ msgstr "" ...@@ -15419,6 +15431,9 @@ msgstr ""
msgid "Integrations|You can now close this window and return to the GitLab for Jira application." msgid "Integrations|You can now close this window and return to the GitLab for Jira application."
msgstr "" msgstr ""
msgid "Integrations|You must have owner or maintainer permissions to link namespaces."
msgstr ""
msgid "Interactive mode" msgid "Interactive mode"
msgstr "" msgstr ""
......
...@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import { addSubscription, removeSubscription } from '~/jira_connect/api'; import { addSubscription, removeSubscription, fetchGroups } from '~/jira_connect/api';
describe('JiraConnect API', () => { describe('JiraConnect API', () => {
let mock; let mock;
...@@ -72,4 +72,36 @@ describe('JiraConnect API', () => { ...@@ -72,4 +72,36 @@ describe('JiraConnect API', () => {
expect(response.data).toEqual(mockResponse); expect(response.data).toEqual(mockResponse);
}); });
}); });
describe('fetchGroups', () => {
const mockGroupsPath = 'groupsPath';
const mockPage = 1;
const mockPerPage = 10;
const makeRequest = () =>
fetchGroups(mockGroupsPath, {
page: mockPage,
perPage: mockPerPage,
});
it('returns success response', async () => {
jest.spyOn(axios, 'get');
mock
.onGet(mockGroupsPath, {
page: mockPage,
per_page: mockPerPage,
})
.replyOnce(httpStatus.OK, mockResponse);
response = await makeRequest();
expect(axios.get).toHaveBeenCalledWith(mockGroupsPath, {
params: {
page: mockPage,
per_page: mockPerPage,
},
});
expect(response.data).toEqual(mockResponse);
});
});
}); });
import { shallowMount } from '@vue/test-utils';
import { GlAvatar } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { mockGroup1 } from '../mock_data';
import GroupsListItem from '~/jira_connect/components/groups_list_item.vue';
describe('GroupsListItem', () => {
let wrapper;
const createComponent = () => {
wrapper = extendedWrapper(
shallowMount(GroupsListItem, {
propsData: {
group: mockGroup1,
},
}),
);
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findGlAvatar = () => wrapper.find(GlAvatar);
const findGroupName = () => wrapper.findByTestId('group-list-item-name');
const findGroupDescription = () => wrapper.findByTestId('group-list-item-description');
it('renders group avatar', () => {
expect(findGlAvatar().exists()).toBe(true);
expect(findGlAvatar().props('src')).toBe(mockGroup1.avatar_url);
});
it('renders group name', () => {
expect(findGroupName().text()).toBe(mockGroup1.full_name);
});
it('renders group description', () => {
expect(findGroupDescription().text()).toBe(mockGroup1.description);
});
});
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { fetchGroups } from '~/jira_connect/api';
import GroupsList from '~/jira_connect/components/groups_list.vue';
import GroupsListItem from '~/jira_connect/components/groups_list_item.vue';
import { mockGroup1, mockGroup2 } from '../mock_data';
jest.mock('~/jira_connect/api', () => {
return {
fetchGroups: jest.fn(),
};
});
describe('GroupsList', () => {
let wrapper;
const mockEmptyResponse = { data: [] };
const createComponent = (options = {}) => {
wrapper = shallowMount(GroupsList, {
...options,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findAllItems = () => wrapper.findAll(GroupsListItem);
const findFirstItem = () => findAllItems().at(0);
const findSecondItem = () => findAllItems().at(1);
describe('isLoading is true', () => {
it('renders loading icon', async () => {
fetchGroups.mockResolvedValue(mockEmptyResponse);
createComponent();
wrapper.setData({ isLoading: true });
await wrapper.vm.$nextTick();
expect(findGlLoadingIcon().exists()).toBe(true);
});
});
describe('no groups returned', () => {
it('renders empty state', async () => {
fetchGroups.mockResolvedValue(mockEmptyResponse);
createComponent();
await waitForPromises();
expect(wrapper.text()).toContain('No available namespaces');
});
});
describe('with groups returned', () => {
it('renders groups list', async () => {
fetchGroups.mockResolvedValue({ data: [mockGroup1, mockGroup2] });
createComponent();
await waitForPromises();
expect(findAllItems().length).toBe(2);
expect(findFirstItem().props('group')).toBe(mockGroup1);
expect(findSecondItem().props('group')).toBe(mockGroup2);
});
});
});
export const mockGroup1 = {
id: 1,
avatar_url: 'avatar.png',
name: 'Gitlab Org',
full_name: 'Gitlab Org',
description: 'Open source software to collaborate on code',
};
export const mockGroup2 = {
id: 2,
avatar_url: 'avatar.png',
name: 'Gitlab Com',
full_name: 'Gitlab Com',
description: 'For GitLab company related projects',
};
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe JiraConnectHelper do
describe '#jira_connect_app_data' do
subject { helper.jira_connect_app_data }
it 'includes Jira Connect app attributes' do
is_expected.to include(
:groups_path
)
end
end
end
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