Commit 34426a7f authored by Nick Kipling's avatar Nick Kipling

Adds package type tabs to packages list

Updated app to include tabs
Moved sorting into dedicated component
Updated list and refactored table headings
Updated API requests to support package_type param
Updated tests
Updated pot file
parent 81c37f85
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { mapState, mapGetters } from 'vuex';
import {
GlTable,
GlPagination,
GlButton,
GlSorting,
GlSortingItem,
GlModal,
GlLink,
GlIcon,
......@@ -15,21 +13,10 @@ import Tracking from '~/tracking';
import { s__, sprintf } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
LIST_KEY_NAME,
LIST_KEY_PROJECT,
LIST_KEY_VERSION,
LIST_KEY_PACKAGE_TYPE,
LIST_KEY_CREATED_AT,
LIST_KEY_ACTIONS,
LIST_LABEL_NAME,
LIST_LABEL_PROJECT,
LIST_LABEL_VERSION,
LIST_LABEL_PACKAGE_TYPE,
LIST_LABEL_CREATED_AT,
LIST_LABEL_ACTIONS,
LIST_ORDER_BY_PACKAGE_TYPE,
ASCENDING_ODER,
DESCENDING_ORDER,
TABLE_HEADER_FIELDS,
} from '../constants';
import { TrackingActions } from '../../shared/constants';
import { packageTypeToTrackCategory } from '../../shared/utils';
......@@ -40,8 +27,6 @@ export default {
components: {
GlTable,
GlPagination,
GlSorting,
GlSortingItem,
GlButton,
GlLink,
TimeAgoTooltip,
......@@ -63,8 +48,6 @@ export default {
totalItems: state => state.pagination.total,
page: state => state.pagination.page,
isGroupPage: state => state.config.isGroupPage,
orderBy: state => state.sorting.orderBy,
sort: state => state.sorting.sort,
isLoading: 'isLoading',
}),
...mapGetters({ list: 'getList' }),
......@@ -76,61 +59,29 @@ export default {
this.$emit('page:changed', value);
},
},
sortText() {
const field = this.sortableFields.find(s => s.orderBy === this.orderBy);
return field ? field.label : '';
},
isSortAscending() {
return this.sort === ASCENDING_ODER;
},
isListEmpty() {
return !this.list || this.list.length === 0;
},
showActions() {
return !this.isGroupPage;
},
sortableFields() {
// This list is filtered in the case of the project page, and the project column is removed
return [
{
key: LIST_KEY_NAME,
label: LIST_LABEL_NAME,
orderBy: LIST_KEY_NAME,
class: ['text-left'],
},
{
key: LIST_KEY_PROJECT,
label: LIST_LABEL_PROJECT,
orderBy: LIST_KEY_PROJECT,
class: ['text-left'],
},
{
key: LIST_KEY_VERSION,
label: LIST_LABEL_VERSION,
orderBy: LIST_KEY_VERSION,
class: ['text-center'],
},
{
key: LIST_KEY_PACKAGE_TYPE,
label: LIST_LABEL_PACKAGE_TYPE,
orderBy: LIST_ORDER_BY_PACKAGE_TYPE,
class: ['text-center'],
},
{
key: LIST_KEY_CREATED_AT,
label: LIST_LABEL_CREATED_AT,
orderBy: LIST_KEY_CREATED_AT,
class: this.showActions ? ['text-center'] : ['text-right'],
},
].filter(f => f.key !== LIST_KEY_PROJECT || this.isGroupPage);
},
headerFields() {
const fields = TABLE_HEADER_FIELDS.filter(
f => f.key !== LIST_KEY_PROJECT || this.isGroupPage,
);
if (this.showActions) {
const actions = {
key: LIST_KEY_ACTIONS,
label: LIST_LABEL_ACTIONS,
tdClass: ['text-right'],
};
return this.showActions ? [...this.sortableFields, actions] : this.sortableFields;
return [...fields, actions];
}
fields[fields.length - 1].class = ['text-right'];
return fields;
},
modalAction() {
return s__('PackageRegistry|Delete package');
......@@ -157,16 +108,6 @@ export default {
},
},
methods: {
...mapActions(['setSorting']),
onDirectionChange() {
const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ODER;
this.setSorting({ sort });
this.$emit('sort:changed');
},
onSortItemClick(item) {
this.setSorting({ orderBy: item });
this.$emit('sort:changed');
},
setItemToBeDeleted(item) {
this.itemToBeDeleted = { ...item };
this.track(TrackingActions.REQUEST_DELETE_PACKAGE);
......@@ -190,22 +131,6 @@ export default {
<slot v-if="isListEmpty && !isLoading" name="empty-state"></slot>
<template v-else>
<gl-sorting
class="my-3 align-self-end"
:text="sortText"
:is-ascending="isSortAscending"
@sortDirectionChange="onDirectionChange"
>
<gl-sorting-item
v-for="item in sortableFields"
ref="packageListSortItem"
:key="item.key"
@click="onSortItemClick(item.orderBy)"
>
{{ item.label }}
</gl-sorting-item>
</gl-sorting>
<gl-table
:items="list"
:fields="headerFields"
......
<script>
import { mapActions, mapState } from 'vuex';
import { GlEmptyState } from '@gitlab/ui';
import { GlEmptyState, GlTab, GlTabs } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import PackageList from './packages_list.vue';
import PackageSort from './packages_sort.vue';
import { PACKAGE_REGISTRY_TABS } from '../constants';
export default {
components: {
GlEmptyState,
GlTab,
GlTabs,
PackageList,
PackageSort,
},
computed: {
...mapState({
......@@ -28,28 +33,41 @@ export default {
false,
);
},
tabsToRender() {
return PACKAGE_REGISTRY_TABS;
},
},
mounted() {
this.requestPackagesList();
},
methods: {
...mapActions(['requestPackagesList', 'requestDeletePackage']),
...mapActions(['requestPackagesList', 'requestDeletePackage', 'setSelectedType']),
onPageChanged(page) {
return this.requestPackagesList({ page });
},
onPackageDeleteRequest(item) {
return this.requestDeletePackage(item);
},
tabChanged(e) {
const selectedType = PACKAGE_REGISTRY_TABS[e];
this.setSelectedType(selectedType);
this.requestPackagesList();
},
},
};
</script>
<template>
<package-list
@page:changed="onPageChanged"
@package:delete="onPackageDeleteRequest"
@sort:changed="requestPackagesList"
>
<gl-tabs @input="tabChanged">
<template #tabs-end>
<div class="align-self-center ml-auto">
<package-sort @sort:changed="requestPackagesList" />
</div>
</template>
<gl-tab v-for="(tab, index) in tabsToRender" :key="index" :title="tab.title">
<package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
<template #empty-state>
<gl-empty-state
:title="s__('PackageRegistry|There are no packages yet')"
......@@ -61,4 +79,6 @@ export default {
</gl-empty-state>
</template>
</package-list>
</gl-tab>
</gl-tabs>
</template>
<script>
import { GlSorting, GlSortingItem } from '@gitlab/ui';
import {
LIST_KEY_PROJECT,
ASCENDING_ODER,
DESCENDING_ORDER,
TABLE_HEADER_FIELDS,
} from '../constants';
import { mapState, mapActions } from 'vuex';
export default {
name: 'PackageSort',
components: {
GlSorting,
GlSortingItem,
},
computed: {
...mapState({
isGroupPage: state => state.config.isGroupPage,
orderBy: state => state.sorting.orderBy,
sort: state => state.sorting.sort,
}),
sortText() {
const field = this.sortableFields.find(s => s.orderBy === this.orderBy);
return field ? field.label : '';
},
sortableFields() {
// This list is filtered in the case of the project page, and the project column is removed
return TABLE_HEADER_FIELDS.filter(f => f.key !== LIST_KEY_PROJECT || this.isGroupPage);
},
isSortAscending() {
return this.sort === ASCENDING_ODER;
},
},
methods: {
...mapActions(['setSorting']),
onDirectionChange() {
const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ODER;
this.setSorting({ sort });
this.$emit('sort:changed');
},
onSortItemClick(item) {
this.setSorting({ orderBy: item });
this.$emit('sort:changed');
},
},
};
</script>
<template>
<gl-sorting
:text="sortText"
:is-ascending="isSortAscending"
@sortDirectionChange="onDirectionChange"
>
<gl-sorting-item
v-for="item in sortableFields"
ref="packageListSortItem"
:key="item.key"
@click="onSortItemClick(item.orderBy)"
>
{{ item.label }}
</gl-sorting-item>
</gl-sorting>
</template>
import { __ } from '~/locale';
import { __, s__ } from '~/locale';
export const FETCH_PACKAGES_LIST_ERROR_MESSAGE = __(
'Something went wrong while fetching the packages list.',
......@@ -33,3 +33,59 @@ export const DESCENDING_ORDER = 'desc';
// The following is not translated because it is used to build a JavaScript exception error message
export const MISSING_DELETE_PATH_ERROR = 'Missing delete_api_path link';
export const TABLE_HEADER_FIELDS = [
{
key: LIST_KEY_NAME,
label: LIST_LABEL_NAME,
orderBy: LIST_KEY_NAME,
class: ['text-left'],
},
{
key: LIST_KEY_PROJECT,
label: LIST_LABEL_PROJECT,
orderBy: LIST_KEY_PROJECT,
class: ['text-left'],
},
{
key: LIST_KEY_VERSION,
label: LIST_LABEL_VERSION,
orderBy: LIST_KEY_VERSION,
class: ['text-center'],
},
{
key: LIST_KEY_PACKAGE_TYPE,
label: LIST_LABEL_PACKAGE_TYPE,
orderBy: LIST_ORDER_BY_PACKAGE_TYPE,
class: ['text-center'],
},
{
key: LIST_KEY_CREATED_AT,
label: LIST_LABEL_CREATED_AT,
orderBy: LIST_KEY_CREATED_AT,
class: ['text-center'],
},
];
export const PACKAGE_REGISTRY_TABS = [
{
title: __('All'),
type: null,
},
{
title: s__('PackageRegistry|Conan'),
type: 'conan',
},
{
title: s__('PackageRegistry|Maven'),
type: 'maven',
},
{
title: s__('PackageRegistry|NPM'),
type: 'npm',
},
{
title: s__('PackageRegistry|NuGet'),
type: 'nuget',
},
];
......@@ -14,20 +14,24 @@ import {
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
export const setLoading = ({ commit }, data) => commit(types.SET_MAIN_LOADING, data);
export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data);
export const setSelectedType = ({ commit }, data) => commit(types.SET_SELECTED_TYPE, data);
export const receivePackagesListSuccess = ({ commit }, { data, headers }) => {
commit(types.SET_PACKAGE_LIST_SUCCESS, data);
commit(types.SET_PAGINATION, headers);
};
export const requestPackagesList = ({ dispatch, state }, pagination = {}) => {
export const requestPackagesList = ({ dispatch, state }, params = {}) => {
dispatch('setLoading', true);
const { page = DEFAULT_PAGE, per_page = DEFAULT_PAGE_SIZE } = pagination;
const { page = DEFAULT_PAGE, per_page = DEFAULT_PAGE_SIZE } = params;
const { sort, orderBy } = state.sorting;
const type = state.selectedType?.type?.toLowerCase();
const packageType = { package_type: type };
const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages';
return Api[apiMethod](state.config.resourceId, {
params: { page, per_page, sort, order_by: orderBy },
params: { page, per_page, sort, order_by: orderBy, ...packageType },
})
.then(({ data, headers }) => {
dispatch('receivePackagesListSuccess', { data, headers });
......
......@@ -4,3 +4,4 @@ export const SET_PACKAGE_LIST_SUCCESS = 'SET_PACKAGE_LIST_SUCCESS';
export const SET_PAGINATION = 'SET_PAGINATION';
export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
export const SET_SORTING = 'SET_SORTING';
export const SET_SELECTED_TYPE = 'SET_SELECTED_TYPE';
......@@ -26,4 +26,8 @@ export default {
[types.SET_SORTING](state, sorting) {
state.sorting = { ...state.sorting, ...sorting };
},
[types.SET_SELECTED_TYPE](state, type) {
state.selectedType = type;
},
};
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`packages_list_app renders 1`] = `
<div>
<b-tabs-stub
activenavitemclass="gl-tab-nav-item-active gl-tab-nav-item-active-indigo"
class="gl-tabs"
contentclass=",gl-tab-content"
navclass="gl-tabs-nav"
nofade="true"
nonavstyle="true"
tag="div"
>
<template>
<b-tab-stub
buttonid=""
tag="div"
title="All"
titlelinkclass="gl-tab-nav-item"
>
<template>
<div>
<div
class="row empty-state"
>
<div
class="col-12"
>
<div
class="svg-250 svg-content"
>
<img
alt="There are no packages yet"
class=""
src="helpSvg"
/>
</div>
</div>
<div
class="col-12"
>
<div
class="text-content"
>
<h4
class="center"
style=""
>
There are no packages yet
</h4>
<p
class="center"
style=""
>
<p>
Learn how to
<a
href="helpUrl"
target="_blank"
>
publish and share your packages
</a>
with GitLab.
</p>
</p>
<div
class="text-center"
>
<!---->
<!---->
</div>
</div>
</div>
</div>
</div>
</template>
</b-tab-stub>
<b-tab-stub
buttonid=""
tag="div"
title="Conan"
titlelinkclass="gl-tab-nav-item"
>
<template>
<div>
<div
class="row empty-state"
>
<div
class="col-12"
>
<div
class="svg-250 svg-content"
>
<img
alt="There are no packages yet"
class=""
src="helpSvg"
/>
</div>
</div>
<div
class="col-12"
>
<div
class="text-content"
>
<h4
class="center"
style=""
>
There are no packages yet
</h4>
<p
class="center"
style=""
>
<p>
Learn how to
<a
href="helpUrl"
target="_blank"
>
publish and share your packages
</a>
with GitLab.
</p>
</p>
<div
class="text-center"
>
<!---->
<!---->
</div>
</div>
</div>
</div>
</div>
</template>
</b-tab-stub>
<b-tab-stub
buttonid=""
tag="div"
title="Maven"
titlelinkclass="gl-tab-nav-item"
>
<template>
<div>
<div
class="row empty-state"
>
<div
class="col-12"
>
<div
class="svg-250 svg-content"
>
<img
alt="There are no packages yet"
class=""
src="helpSvg"
/>
</div>
</div>
<div
class="col-12"
>
<div
class="text-content"
>
<h4
class="center"
style=""
>
There are no packages yet
</h4>
<p
class="center"
style=""
>
<p>
Learn how to
<a
href="helpUrl"
target="_blank"
>
publish and share your packages
</a>
with GitLab.
</p>
</p>
<div
class="text-center"
>
<!---->
<!---->
</div>
</div>
</div>
</div>
</div>
</template>
</b-tab-stub>
<b-tab-stub
buttonid=""
tag="div"
title="NPM"
titlelinkclass="gl-tab-nav-item"
>
<template>
<div>
<div
class="row empty-state"
>
......@@ -58,5 +277,83 @@ exports[`packages_list_app renders 1`] = `
</div>
</div>
</div>
</div>
</div>
</template>
</b-tab-stub>
<b-tab-stub
buttonid=""
tag="div"
title="NuGet"
titlelinkclass="gl-tab-nav-item"
>
<template>
<div>
<div
class="row empty-state"
>
<div
class="col-12"
>
<div
class="svg-250 svg-content"
>
<img
alt="There are no packages yet"
class=""
src="helpSvg"
/>
</div>
</div>
<div
class="col-12"
>
<div
class="text-content"
>
<h4
class="center"
style=""
>
There are no packages yet
</h4>
<p
class="center"
style=""
>
<p>
Learn how to
<a
href="helpUrl"
target="_blank"
>
publish and share your packages
</a>
with GitLab.
</p>
</p>
<div
class="text-center"
>
<!---->
<!---->
</div>
</div>
</div>
</div>
</div>
</template>
</b-tab-stub>
</template>
<template>
<div
class="align-self-center ml-auto"
>
<package-sort-stub />
</div>
</template>
</b-tabs-stub>
`;
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
import { GlEmptyState, GlTab, GlTabs } from '@gitlab/ui';
import PackageListApp from 'ee/packages/list/components/packages_list_app.vue';
const localVue = createLocalVue();
......@@ -18,6 +18,7 @@ describe('packages_list_app', () => {
const emptyListHelpUrl = 'helpUrl';
const findListComponent = () => wrapper.find(PackageList);
const findTabComponent = (index = 0) => wrapper.findAll(GlTab).at(index);
const mountComponent = () => {
wrapper = shallowMount(PackageListApp, {
......@@ -27,6 +28,8 @@ describe('packages_list_app', () => {
GlEmptyState,
GlLoadingIcon,
PackageList,
GlTab,
GlTabs,
},
});
};
......@@ -81,4 +84,18 @@ describe('packages_list_app', () => {
list.vm.$emit('sort:changed');
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
});
describe('tab change', () => {
it('calls requestPackagesList when all tab is clicked', () => {
findTabComponent().trigger('click');
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
});
it('calls requestPackagesList when a package type tab is clicked', () => {
findTabComponent(1).trigger('click');
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
});
});
});
import Vuex from 'vuex';
import { last } from 'lodash';
import { GlTable, GlSorting, GlPagination, GlModal } from '@gitlab/ui';
import { GlTable, GlPagination, GlModal } from '@gitlab/ui';
import Tracking from '~/tracking';
import { mount, createLocalVue } from '@vue/test-utils';
import PackagesList from 'ee/packages/list/components/packages_list.vue';
......@@ -24,10 +24,8 @@ describe('packages_list', () => {
const findPackagesListLoader = () => wrapper.find(PackagesListLoader);
const findFirstActionColumn = () => wrapper.find({ ref: 'action-delete' });
const findPackageListTable = () => wrapper.find(GlTable);
const findPackageListSorting = () => wrapper.find(GlSorting);
const findPackageListPagination = () => wrapper.find(GlPagination);
const findPackageListDeleteModal = () => wrapper.find(GlModal);
const findSortingItems = () => wrapper.findAll(GlSortingItem);
const findFirstProjectColumn = () => wrapper.find({ ref: 'col-project' });
const findPackageTags = () => wrapper.findAll(PackageTags);
const findEmptySlot = () => wrapper.find({ name: 'empty-slot-stub' });
......@@ -127,11 +125,6 @@ describe('packages_list', () => {
mountComponent();
});
it('contains a sorting component', () => {
const sorting = findPackageListSorting();
expect(sorting.exists()).toBe(true);
});
it('contains a table component', () => {
const sorting = findPackageListTable();
expect(sorting.exists()).toBe(true);
......@@ -222,35 +215,6 @@ describe('packages_list', () => {
});
});
describe('sorting component', () => {
let sorting;
let sortingItems;
beforeEach(() => {
mountComponent();
sorting = findPackageListSorting();
sortingItems = findSortingItems();
});
it('has all the sortable items', () => {
expect(sortingItems.length).toEqual(wrapper.vm.sortableFields.length);
});
it('on sort change set sorting in vuex and emit event', () => {
sorting.vm.$emit('sortDirectionChange');
expect(store.dispatch).toHaveBeenCalledWith('setSorting', { sort: 'asc' });
expect(wrapper.emitted('sort:changed')).toBeTruthy();
});
it('on sort item click set sorting and emit event', () => {
const item = sortingItems.at(0);
const { orderBy } = wrapper.vm.sortableFields[0];
item.vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('setSorting', { orderBy });
expect(wrapper.emitted('sort:changed')).toBeTruthy();
});
});
describe('pagination component', () => {
let pagination;
let modelEvent;
......
import Vuex from 'vuex';
import { GlSorting } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import PackagesSort from 'ee/packages/list/components/packages_sort.vue';
import stubChildren from 'helpers/stub_children';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('packages_sort', () => {
let wrapper;
let store;
let sorting;
let sortingItems;
const GlSortingItem = { name: 'sorting-item-stub', template: '<div><slot></slot></div>' };
const findPackageListSorting = () => wrapper.find(GlSorting);
const findSortingItems = () => wrapper.findAll(GlSortingItem);
const createStore = isGroupPage => {
const state = {
config: {
isGroupPage,
},
sorting: {
orderBy: 'version',
sort: 'desc',
},
};
store = new Vuex.Store({
state,
});
store.dispatch = jest.fn();
};
const mountComponent = (isGroupPage = false) => {
createStore(isGroupPage);
wrapper = mount(PackagesSort, {
localVue,
store,
stubs: {
...stubChildren(PackagesSort),
GlSortingItem,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when is in projects', () => {
beforeEach(() => {
mountComponent();
sorting = findPackageListSorting();
sortingItems = findSortingItems();
});
it('has all the sortable items', () => {
expect(sortingItems.length).toEqual(wrapper.vm.sortableFields.length);
});
it('on sort change set sorting in vuex and emit event', () => {
sorting.vm.$emit('sortDirectionChange');
expect(store.dispatch).toHaveBeenCalledWith('setSorting', { sort: 'asc' });
expect(wrapper.emitted('sort:changed')).toBeTruthy();
});
it('on sort item click set sorting and emit event', () => {
const item = sortingItems.at(0);
const { orderBy } = wrapper.vm.sortableFields[0];
item.vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('setSorting', { orderBy });
expect(wrapper.emitted('sort:changed')).toBeTruthy();
});
});
describe('when is in group', () => {
beforeEach(() => {
mountComponent(true);
sorting = findPackageListSorting();
sortingItems = findSortingItems();
});
it('has all the sortable items', () => {
expect(sortingItems.length).toEqual(wrapper.vm.sortableFields.length);
});
});
});
......@@ -72,6 +72,38 @@ describe('Actions Package list store', () => {
);
});
it('should fetch packages of a certain type when selectedType is present', done => {
const packageType = 'maven';
testAction(
actions.requestPackagesList,
undefined,
{
config: { isGroupPage: false, resourceId: 1 },
sorting,
selectedType: { type: packageType },
},
[],
[
{ type: 'setLoading', payload: true },
{ type: 'receivePackagesListSuccess', payload: { data: 'foo', headers } },
{ type: 'setLoading', payload: false },
],
() => {
expect(Api.projectPackages).toHaveBeenCalledWith(1, {
params: {
page: 1,
per_page: 20,
sort: sorting.sort,
order_by: sorting.orderBy,
package_type: packageType,
},
});
done();
},
);
});
it('should create flash on API error', done => {
Api.projectPackages = jest.fn().mockRejectedValue();
testAction(
......
......@@ -77,4 +77,11 @@ describe('Mutations Registry Store', () => {
expect(mockState.sorting).toEqual({ ...mockState.sorting, orderBy: 'foo' });
});
});
describe('SET_SELECTED_TYPE', () => {
it('should set the selected type', () => {
mutations[types.SET_SELECTED_TYPE](mockState, { type: 'maven' });
expect(mockState.selectedType).toEqual({ type: 'maven' });
});
});
});
......@@ -13855,6 +13855,9 @@ msgstr ""
msgid "PackageRegistry|Add NuGet Source"
msgstr ""
msgid "PackageRegistry|Conan"
msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
......@@ -13918,12 +13921,21 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
msgid "PackageRegistry|Maven"
msgstr ""
msgid "PackageRegistry|Maven Command"
msgstr ""
msgid "PackageRegistry|Maven XML"
msgstr ""
msgid "PackageRegistry|NPM"
msgstr ""
msgid "PackageRegistry|NuGet"
msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
......
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