Commit fb25aab7 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch '258666-removing-coming-soon-section-from-package-registry-ui' into 'master'

Remove coming soon section from Package Registry UI

See merge request gitlab-org/gitlab!44271
parents 1343456a 102d0e57
/**
* Context:
* https://gitlab.com/gitlab-org/gitlab/-/issues/198524
* https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29491
*
*/
/**
* Constants
*
* LABEL_NAMES - an array of labels to filter issues in the GraphQL query
* WORKFLOW_PREFIX - the prefix for workflow labels
* ACCEPTING_CONTRIBUTIONS_TITLE - the accepting contributions label
*/
export const LABEL_NAMES = ['Package::Coming soon'];
const WORKFLOW_PREFIX = 'workflow::';
const ACCEPTING_CONTRIBUTIONS_TITLE = 'accepting merge requests';
const setScoped = (label, scoped) => (label ? { ...label, scoped } : label);
/**
* Finds workflow:: scoped labels and returns the first or null.
* @param {Object[]} labels Labels from the issue
*/
export const findWorkflowLabel = (labels = []) =>
labels.find(l => l.title.toLowerCase().includes(WORKFLOW_PREFIX.toLowerCase()));
/**
* Determines if an issue is accepting community contributions by checking if
* the "Accepting merge requests" label is present.
* @param {Object[]} labels
*/
export const findAcceptingContributionsLabel = (labels = []) =>
labels.find(l => l.title.toLowerCase() === ACCEPTING_CONTRIBUTIONS_TITLE.toLowerCase());
/**
* Formats the GraphQL response into the format that the view template expects.
* @param {Object} data GraphQL response
*/
export const toViewModel = data => {
// This just flatterns the issues -> nodes and labels -> nodes hierarchy
// into an array of objects.
const issues = (data.project?.issues?.nodes || []).map(i => ({
...i,
labels: (i.labels?.nodes || []).map(node => node),
}));
return issues.map(x => ({
...x,
labels: [
setScoped(findWorkflowLabel(x.labels), true),
setScoped(findAcceptingContributionsLabel(x.labels), false),
].filter(Boolean),
}));
};
<script>
import {
GlAlert,
GlEmptyState,
GlIcon,
GlLabel,
GlLink,
GlSkeletonLoader,
GlSprintf,
} from '@gitlab/ui';
import { ApolloQuery } from 'vue-apollo';
import Tracking from '~/tracking';
import { TrackingActions } from '../../shared/constants';
import { s__ } from '~/locale';
import comingSoonIssuesQuery from './queries/issues.graphql';
import { toViewModel, LABEL_NAMES } from './helpers';
export default {
name: 'ComingSoon',
components: {
GlAlert,
GlEmptyState,
GlIcon,
GlLabel,
GlLink,
GlSkeletonLoader,
GlSprintf,
ApolloQuery,
},
mixins: [Tracking.mixin()],
props: {
illustration: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
suggestedContributionsPath: {
type: String,
required: true,
},
},
computed: {
variables() {
return {
projectPath: this.projectPath,
labelNames: LABEL_NAMES,
};
},
},
mounted() {
this.track(TrackingActions.COMING_SOON_REQUESTED);
},
methods: {
onIssueLinkClick(issueIid, label) {
this.track(TrackingActions.COMING_SOON_LIST, {
label,
value: issueIid,
});
},
onDocsLinkClick() {
this.track(TrackingActions.COMING_SOON_HELP);
},
},
loadingRows: 5,
i18n: {
alertTitle: s__('PackageRegistry|Upcoming package managers'),
alertIntro: s__(
"PackageRegistry|Is your favorite package manager missing? We'd love your help in building first-class support for it into GitLab! %{contributionLinkStart}Visit the contribution documentation%{contributionLinkEnd} to learn more about how to build support for new package managers into GitLab. Below is a list of package managers that are on our radar.",
),
emptyStateTitle: s__('PackageRegistry|No upcoming issues'),
emptyStateDescription: s__('PackageRegistry|There are no upcoming issues to display.'),
},
comingSoonIssuesQuery,
toViewModel,
};
</script>
<template>
<apollo-query
:query="$options.comingSoonIssuesQuery"
:variables="variables"
:update="$options.toViewModel"
>
<template #default="{ result: { data }, isLoading }">
<div>
<gl-alert :title="$options.i18n.alertTitle" :dismissible="false" variant="tip">
<gl-sprintf :message="$options.i18n.alertIntro">
<template #contributionLink="{ content }">
<gl-link
:href="suggestedContributionsPath"
target="_blank"
@click="onDocsLinkClick"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</gl-alert>
</div>
<div v-if="isLoading" class="gl-display-flex gl-flex-direction-column">
<gl-skeleton-loader
v-for="index in $options.loadingRows"
:key="index"
:width="1000"
:height="80"
preserve-aspect-ratio="xMinYMax meet"
>
<rect width="700" height="10" x="0" y="16" rx="4" />
<rect width="60" height="10" x="0" y="45" rx="4" />
<rect width="60" height="10" x="70" y="45" rx="4" />
</gl-skeleton-loader>
</div>
<template v-else-if="data && data.length">
<div
v-for="issue in data"
:key="issue.iid"
data-testid="issue-row"
class="gl-responsive-table-row gl-flex-direction-column gl-align-items-baseline"
>
<div class="table-section section-100 gl-white-space-normal text-truncate">
<gl-link
data-testid="issue-title-link"
:href="issue.webUrl"
class="gl-text-gray-900 gl-font-weight-bold"
@click="onIssueLinkClick(issue.iid, issue.title)"
>
{{ issue.title }}
</gl-link>
</div>
<div class="table-section section-100 gl-white-space-normal mt-md-3">
<div class="gl-display-flex gl-text-gray-400">
<gl-icon name="issues" class="gl-mr-2" />
<gl-link
data-testid="issue-id-link"
:href="issue.webUrl"
class="gl-text-gray-400 gl-mr-5"
@click="onIssueLinkClick(issue.iid, issue.title)"
>#{{ issue.iid }}</gl-link
>
<div v-if="issue.milestone" class="gl-display-flex gl-align-items-center gl-mr-5">
<gl-icon name="clock" class="gl-mr-2" />
<span data-testid="milestone">{{ issue.milestone.title }}</span>
</div>
<gl-label
v-for="label in issue.labels"
:key="label.title"
class="gl-mr-3"
size="sm"
:background-color="label.color"
:title="label.title"
:scoped="Boolean(label.scoped)"
/>
</div>
</div>
</div>
</template>
<gl-empty-state v-else :title="$options.i18n.emptyStateTitle" :svg-path="illustration">
<template #description>
<p>{{ $options.i18n.emptyStateDescription }}</p>
</template>
</gl-empty-state>
</template>
</apollo-query>
</template>
query getComingSoonIssues($projectPath: ID!, $labelNames: [String]) {
project(fullPath: $projectPath) {
issues(state: opened, labelName: $labelNames) {
nodes {
iid
title
webUrl
labels {
nodes {
title
color
}
}
milestone {
title
}
}
}
}
}
...@@ -9,7 +9,6 @@ import PackageFilter from './packages_filter.vue'; ...@@ -9,7 +9,6 @@ import PackageFilter from './packages_filter.vue';
import PackageList from './packages_list.vue'; import PackageList from './packages_list.vue';
import PackageSort from './packages_sort.vue'; import PackageSort from './packages_sort.vue';
import { PACKAGE_REGISTRY_TABS, DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants'; import { PACKAGE_REGISTRY_TABS, DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
import PackagesComingSoon from '../coming_soon/packages_coming_soon.vue';
import PackageTitle from './package_title.vue'; import PackageTitle from './package_title.vue';
export default { export default {
...@@ -22,14 +21,12 @@ export default { ...@@ -22,14 +21,12 @@ export default {
PackageFilter, PackageFilter,
PackageList, PackageList,
PackageSort, PackageSort,
PackagesComingSoon,
PackageTitle, PackageTitle,
}, },
computed: { computed: {
...mapState({ ...mapState({
emptyListIllustration: state => state.config.emptyListIllustration, emptyListIllustration: state => state.config.emptyListIllustration,
emptyListHelpUrl: state => state.config.emptyListHelpUrl, emptyListHelpUrl: state => state.config.emptyListHelpUrl,
comingSoon: state => state.config.comingSoon,
filterQuery: state => state.filterQuery, filterQuery: state => state.filterQuery,
selectedType: state => state.selectedType, selectedType: state => state.selectedType,
packageHelpUrl: state => state.config.packageHelpUrl, packageHelpUrl: state => state.config.packageHelpUrl,
...@@ -122,14 +119,6 @@ export default { ...@@ -122,14 +119,6 @@ export default {
</template> </template>
</package-list> </package-list>
</gl-tab> </gl-tab>
<gl-tab v-if="comingSoon" :title="__('Coming soon')" lazy>
<packages-coming-soon
:illustration="emptyListIllustration"
:project-path="comingSoon.projectPath"
:suggested-contributions-path="comingSoon.suggestedContributions"
/>
</gl-tab>
</gl-tabs> </gl-tabs>
</div> </div>
</template> </template>
import * as types from './mutation_types'; import * as types from './mutation_types';
import { import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
parseIntPagination,
normalizeHeaders,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
import { GROUP_PAGE_TYPE } from '../constants'; import { GROUP_PAGE_TYPE } from '../constants';
export default { export default {
[types.SET_INITIAL_STATE](state, config) { [types.SET_INITIAL_STATE](state, config) {
const { comingSoonJson, ...rest } = config; const { comingSoonJson, ...rest } = config;
const comingSoonObj = JSON.parse(comingSoonJson);
state.config = { state.config = {
...rest, ...rest,
comingSoon: comingSoonObj && convertObjectPropsToCamelCase(comingSoonObj),
isGroupPage: config.pageType === GROUP_PAGE_TYPE, isGroupPage: config.pageType === GROUP_PAGE_TYPE,
}; };
}, },
......
...@@ -14,9 +14,6 @@ export const TrackingActions = { ...@@ -14,9 +14,6 @@ export const TrackingActions = {
REQUEST_DELETE_PACKAGE: 'request_delete_package', REQUEST_DELETE_PACKAGE: 'request_delete_package',
CANCEL_DELETE_PACKAGE: 'cancel_delete_package', CANCEL_DELETE_PACKAGE: 'cancel_delete_package',
PULL_PACKAGE: 'pull_package', PULL_PACKAGE: 'pull_package',
COMING_SOON_REQUESTED: 'activate_coming_soon_requested',
COMING_SOON_LIST: 'click_coming_soon_issue_link',
COMING_SOON_HELP: 'click_coming_soon_documentation_link',
}; };
export const TrackingCategories = { export const TrackingCategories = {
......
...@@ -34,26 +34,12 @@ module PackagesHelper ...@@ -34,26 +34,12 @@ module PackagesHelper
expose_url(api_v4_group___packages_composer_packages_path(id: group_id, format: '.json')) expose_url(api_v4_group___packages_composer_packages_path(id: group_id, format: '.json'))
end end
def packages_coming_soon_enabled?(resource)
::Feature.enabled?(:packages_coming_soon, resource) && ::Gitlab.dev_env_or_com?
end
def packages_coming_soon_data(resource)
return unless packages_coming_soon_enabled?(resource)
{
project_path: ::Gitlab.com? ? 'gitlab-org/gitlab' : 'gitlab-org/gitlab-test',
suggested_contributions: help_page_path('user/packages/index', anchor: 'suggested-contributions')
}
end
def packages_list_data(type, resource) def packages_list_data(type, resource)
{ {
resource_id: resource.id, resource_id: resource.id,
page_type: type, page_type: type,
empty_list_help_url: help_page_path('user/packages/package_registry/index'), empty_list_help_url: help_page_path('user/packages/package_registry/index'),
empty_list_illustration: image_path('illustrations/no-packages.svg'), empty_list_illustration: image_path('illustrations/no-packages.svg'),
coming_soon_json: packages_coming_soon_data(resource).to_json,
package_help_url: help_page_path('user/packages/index') package_help_url: help_page_path('user/packages/index')
} }
end end
......
---
name: packages_coming_soon
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: false
...@@ -6456,9 +6456,6 @@ msgstr "" ...@@ -6456,9 +6456,6 @@ msgstr ""
msgid "ComboSearch is not defined" msgid "ComboSearch is not defined"
msgstr "" msgstr ""
msgid "Coming soon"
msgstr ""
msgid "Comma-separated, e.g. '1.1.1.1, 2.2.2.0/24'" msgid "Comma-separated, e.g. '1.1.1.1, 2.2.2.0/24'"
msgstr "" msgstr ""
...@@ -18305,9 +18302,6 @@ msgstr "" ...@@ -18305,9 +18302,6 @@ msgstr ""
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file." msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr "" msgstr ""
msgid "PackageRegistry|Is your favorite package manager missing? We'd love your help in building first-class support for it into GitLab! %{contributionLinkStart}Visit the contribution documentation%{contributionLinkEnd} to learn more about how to build support for new package managers into GitLab. Below is a list of package managers that are on our radar."
msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab." msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr "" msgstr ""
...@@ -18329,9 +18323,6 @@ msgstr "" ...@@ -18329,9 +18323,6 @@ msgstr ""
msgid "PackageRegistry|NPM" msgid "PackageRegistry|NPM"
msgstr "" msgstr ""
msgid "PackageRegistry|No upcoming issues"
msgstr ""
msgid "PackageRegistry|NuGet" msgid "PackageRegistry|NuGet"
msgstr "" msgstr ""
...@@ -18377,9 +18368,6 @@ msgstr "" ...@@ -18377,9 +18368,6 @@ msgstr ""
msgid "PackageRegistry|There are no packages yet" msgid "PackageRegistry|There are no packages yet"
msgstr "" msgstr ""
msgid "PackageRegistry|There are no upcoming issues to display."
msgstr ""
msgid "PackageRegistry|There was a problem fetching the details for this package." msgid "PackageRegistry|There was a problem fetching the details for this package."
msgstr "" msgstr ""
...@@ -18395,9 +18383,6 @@ msgstr "" ...@@ -18395,9 +18383,6 @@ msgstr ""
msgid "PackageRegistry|Unable to load package" msgid "PackageRegistry|Unable to load package"
msgstr "" msgstr ""
msgid "PackageRegistry|Upcoming package managers"
msgstr ""
msgid "PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?" msgid "PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?"
msgstr "" msgstr ""
......
import * as comingSoon from '~/packages/list/coming_soon/helpers';
import { fakeIssues, asGraphQLResponse, asViewModel } from './mock_data';
jest.mock('~/api.js');
describe('Coming Soon Helpers', () => {
const [noLabels, acceptingMergeRequestLabel, workflowLabel] = fakeIssues;
describe('toViewModel', () => {
it('formats a GraphQL response correctly', () => {
expect(comingSoon.toViewModel(asGraphQLResponse)).toEqual(asViewModel);
});
});
describe('findWorkflowLabel', () => {
it('finds a workflow label', () => {
expect(comingSoon.findWorkflowLabel(workflowLabel.labels)).toEqual(workflowLabel.labels[0]);
});
it("returns undefined when there isn't one", () => {
expect(comingSoon.findWorkflowLabel(noLabels.labels)).toBeUndefined();
});
});
describe('findAcceptingContributionsLabel', () => {
it('finds the correct label when it exists', () => {
expect(comingSoon.findAcceptingContributionsLabel(acceptingMergeRequestLabel.labels)).toEqual(
acceptingMergeRequestLabel.labels[0],
);
});
it("returns undefined when there isn't one", () => {
expect(comingSoon.findAcceptingContributionsLabel(noLabels.labels)).toBeUndefined();
});
});
});
export const fakeIssues = [
{
id: 1,
iid: 1,
title: 'issue one',
webUrl: 'foo',
},
{
id: 2,
iid: 2,
title: 'issue two',
labels: [{ title: 'Accepting merge requests', color: '#69d100' }],
milestone: {
title: '12.10',
},
webUrl: 'foo',
},
{
id: 3,
iid: 3,
title: 'issue three',
labels: [{ title: 'workflow::In dev', color: '#428bca' }],
webUrl: 'foo',
},
{
id: 4,
iid: 4,
title: 'issue four',
labels: [
{ title: 'Accepting merge requests', color: '#69d100' },
{ title: 'workflow::In dev', color: '#428bca' },
],
webUrl: 'foo',
},
];
export const asGraphQLResponse = {
project: {
issues: {
nodes: fakeIssues.map(x => ({
...x,
labels: {
nodes: x.labels,
},
})),
},
},
};
export const asViewModel = [
{
...fakeIssues[0],
labels: [],
},
{
...fakeIssues[1],
labels: [
{
title: 'Accepting merge requests',
color: '#69d100',
scoped: false,
},
],
},
{
...fakeIssues[2],
labels: [
{
title: 'workflow::In dev',
color: '#428bca',
scoped: true,
},
],
},
{
...fakeIssues[3],
labels: [
{
title: 'workflow::In dev',
color: '#428bca',
scoped: true,
},
{
title: 'Accepting merge requests',
color: '#69d100',
scoped: false,
},
],
},
];
import { GlEmptyState, GlSkeletonLoader, GlLabel } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import VueApollo, { ApolloQuery } from 'vue-apollo';
import ComingSoon from '~/packages/list/coming_soon/packages_coming_soon.vue';
import { TrackingActions } from '~/packages/shared/constants';
import { asViewModel } from './mock_data';
import Tracking from '~/tracking';
jest.mock('~/packages/list/coming_soon/helpers.js');
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('packages_coming_soon', () => {
let wrapper;
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findAllIssues = () => wrapper.findAll('[data-testid="issue-row"]');
const findIssuesData = () =>
findAllIssues().wrappers.map(x => {
const titleLink = x.find('[data-testid="issue-title-link"]');
const milestone = x.find('[data-testid="milestone"]');
const issueIdLink = x.find('[data-testid="issue-id-link"]');
const labels = x.findAll(GlLabel);
const issueId = Number(issueIdLink.text().substr(1));
return {
id: issueId,
iid: issueId,
title: titleLink.text(),
webUrl: titleLink.attributes('href'),
labels: labels.wrappers.map(label => ({
color: label.props('backgroundColor'),
title: label.props('title'),
scoped: label.props('scoped'),
})),
...(milestone.exists() ? { milestone: { title: milestone.text() } } : {}),
};
});
const findIssueTitleLink = () => wrapper.find('[data-testid="issue-title-link"]');
const findIssueIdLink = () => wrapper.find('[data-testid="issue-id-link"]');
const findEmptyState = () => wrapper.find(GlEmptyState);
const mountComponent = (testParams = {}) => {
const $apolloData = {
loading: testParams.isLoading || false,
};
wrapper = mount(ComingSoon, {
localVue,
propsData: {
illustration: 'foo',
projectPath: 'foo',
suggestedContributionsPath: 'foo',
},
stubs: {
ApolloQuery,
GlLink: true,
},
mocks: {
$apolloData,
},
});
// Mock the GraphQL query result
wrapper.find(ApolloQuery).setData({
result: {
data: testParams.issues || asViewModel,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when loading', () => {
beforeEach(() => mountComponent({ isLoading: true }));
it('renders the skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(true);
});
});
describe('when there are no issues', () => {
beforeEach(() => mountComponent({ issues: [] }));
it('renders the empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
});
describe('when there are issues', () => {
beforeEach(() => mountComponent());
it('renders each issue', () => {
expect(findIssuesData()).toEqual(asViewModel);
});
});
describe('tracking', () => {
const firstIssue = asViewModel[0];
let eventSpy;
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
mountComponent();
});
it('tracks when mounted', () => {
expect(eventSpy).toHaveBeenCalledWith(undefined, TrackingActions.COMING_SOON_REQUESTED, {});
});
it('tracks when an issue title link is clicked', () => {
eventSpy.mockClear();
findIssueTitleLink().vm.$emit('click');
expect(eventSpy).toHaveBeenCalledWith(undefined, TrackingActions.COMING_SOON_LIST, {
label: firstIssue.title,
value: firstIssue.iid,
});
});
it('tracks when an issue id link is clicked', () => {
eventSpy.mockClear();
findIssueIdLink().vm.$emit('click');
expect(eventSpy).toHaveBeenCalledWith(undefined, TrackingActions.COMING_SOON_LIST, {
label: firstIssue.title,
value: firstIssue.iid,
});
});
});
});
...@@ -444,8 +444,6 @@ exports[`packages_list_app renders 1`] = ` ...@@ -444,8 +444,6 @@ exports[`packages_list_app renders 1`] = `
</div> </div>
</template> </template>
</b-tab-stub> </b-tab-stub>
<!---->
</template> </template>
<template> <template>
<div <div
......
...@@ -18,7 +18,6 @@ describe('Mutations Registry Store', () => { ...@@ -18,7 +18,6 @@ describe('Mutations Registry Store', () => {
userCanDelete: '', userCanDelete: '',
emptyListIllustration: 'foo', emptyListIllustration: 'foo',
emptyListHelpUrl: 'baz', emptyListHelpUrl: 'baz',
comingSoonJson: '{ "project_path": "gitlab-org/gitlab-test" }',
}; };
const expectedState = { const expectedState = {
......
...@@ -51,38 +51,4 @@ RSpec.describe PackagesHelper do ...@@ -51,38 +51,4 @@ RSpec.describe PackagesHelper do
expect(url).to eq("#{base_url}group/1/-/packages/composer/packages.json") expect(url).to eq("#{base_url}group/1/-/packages/composer/packages.json")
end end
end end
describe 'packages_coming_soon_enabled?' do
it 'returns false when the feature flag is disabled' do
stub_feature_flags(packages_coming_soon: false)
expect(helper.packages_coming_soon_enabled?(project)).to eq(false)
end
it 'returns false when not on dev or gitlab.com' do
expect(helper.packages_coming_soon_enabled?(project)).to eq(false)
end
end
describe 'packages_coming_soon_data' do
let_it_be(:group) { create(:group) }
before do
allow(Gitlab).to receive(:dev_env_or_com?) { true }
end
it 'returns the gitlab project on gitlab.com' do
allow(Gitlab).to receive(:com?) { true }
expect(helper.packages_coming_soon_data(project)).to include({ project_path: 'gitlab-org/gitlab' })
end
it 'returns the test project when not on gitlab.com' do
expect(helper.packages_coming_soon_data(project)).to include({ project_path: 'gitlab-org/gitlab-test' })
end
it 'works correctly with a group' do
expect(helper.packages_coming_soon_data(group)).to include({ project_path: 'gitlab-org/gitlab-test' })
end
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