Commit 4b9ff9d1 authored by Mike Greiling's avatar Mike Greiling

Merge branch 'nfriend-camel-case-releases' into 'master'

Reference all Release data using `camelCase`

See merge request gitlab-org/gitlab!24700
parents db447fd8 95fcaa50
......@@ -76,7 +76,7 @@ export default {
<div v-else-if="shouldRenderSuccessState" class="js-success-state">
<release-block
v-for="(release, index) in releases"
:key="release.tag_name"
:key="release.tagName"
:release="release"
:class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
/>
......
......@@ -25,16 +25,16 @@ export default {
},
computed: {
evidenceTitle() {
return sprintf(__('%{tag}-evidence.json'), { tag: this.release.tag_name });
return sprintf(__('%{tag}-evidence.json'), { tag: this.release.tagName });
},
evidenceUrl() {
return this.release.assets && this.release.assets.evidence_file_path;
return this.release.assets && this.release.assets.evidenceFilePath;
},
shortSha() {
return truncateSha(this.sha);
},
sha() {
return this.release.evidence_sha;
return this.release.evidenceSha;
},
},
};
......
......@@ -38,13 +38,13 @@ export default {
},
computed: {
id() {
return slugify(this.release.tag_name);
return slugify(this.release.tagName);
},
assets() {
return this.release.assets || {};
},
hasEvidence() {
return Boolean(this.release.evidence_sha);
return Boolean(this.release.evidenceSha);
},
milestones() {
return this.release.milestones || [];
......@@ -102,7 +102,7 @@ export default {
<evidence-block v-if="hasEvidence && shouldShowEvidence" :release="release" />
<div ref="gfm-content" class="card-text prepend-top-default">
<div v-html="release.description_html"></div>
<div v-html="release.descriptionHtml"></div>
</div>
</div>
......@@ -110,11 +110,11 @@ export default {
v-if="shouldShowFooter"
class="card-footer"
:commit="release.commit"
:commit-path="release.commit_path"
:tag-name="release.tag_name"
:tag-path="release.tag_path"
:commit-path="release.commitPath"
:tag-name="release.tagName"
:tag-path="release.tagPath"
:author="release.author"
:released-at="release.released_at"
:released-at="release.releasedAt"
/>
</div>
</template>
......@@ -31,8 +31,8 @@ export default {
<template #user>
<user-avatar-link
class="prepend-left-4"
:link-href="author.web_url"
:img-src="author.avatar_url"
:link-href="author.webUrl"
:img-src="author.avatarUrl"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
/>
......
......@@ -66,9 +66,9 @@ export default {
<icon ref="commitIcon" name="commit" class="mr-1" />
<div v-gl-tooltip.bottom :title="commit.title">
<gl-link v-if="commitPath" :href="commitPath">
{{ commit.short_id }}
{{ commit.shortId }}
</gl-link>
<span v-else>{{ commit.short_id }}</span>
<span v-else>{{ commit.shortId }}</span>
</div>
</div>
......@@ -100,8 +100,8 @@ export default {
<div v-if="author" class="d-flex">
<span class="text-secondary">{{ __('by') }}&nbsp;</span>
<user-avatar-link
:link-href="author.web_url"
:img-src="author.avatar_url"
:link-href="author.webUrl"
:img-src="author.avatarUrl"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
tooltip-placement="bottom"
......
......@@ -20,10 +20,10 @@ export default {
},
computed: {
editLink() {
return this.release._links?.edit_url;
return this.release.Links?.editUrl;
},
selfLink() {
return this.release._links?.self;
return this.release.Links?.self;
},
},
};
......@@ -36,7 +36,7 @@ export default {
{{ release.name }}
</gl-link>
<template v-else>{{ release.name }}</template>
<gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
<gl-badge v-if="release.upcomingRelease" variant="warning" class="align-middle">{{
__('Upcoming Release')
}}</gl-badge>
</h2>
......
......@@ -32,21 +32,21 @@ export default {
return this.release.commit || {};
},
commitUrl() {
return this.release.commit_path;
return this.release.commitPath;
},
hasAuthor() {
return Boolean(this.author);
},
releasedTimeAgo() {
return sprintf(__('released %{time}'), {
time: this.timeFormatted(this.release.released_at),
time: this.timeFormatted(this.release.releasedAt),
});
},
shouldRenderMilestones() {
return Boolean(this.release.milestones?.length);
},
tagUrl() {
return this.release.tag_path;
return this.release.tagPath;
},
},
};
......@@ -57,24 +57,24 @@ export default {
<div class="append-right-8">
<icon name="commit" class="align-middle" />
<gl-link v-if="commitUrl" v-gl-tooltip.bottom :title="commit.title" :href="commitUrl">
{{ commit.short_id }}
{{ commit.shortId }}
</gl-link>
<span v-else v-gl-tooltip.bottom :title="commit.title">{{ commit.short_id }}</span>
<span v-else v-gl-tooltip.bottom :title="commit.title">{{ commit.shortId }}</span>
</div>
<div class="append-right-8">
<icon name="tag" class="align-middle" />
<gl-link v-if="tagUrl" v-gl-tooltip.bottom :title="__('Tag')" :href="tagUrl">
{{ release.tag_name }}
{{ release.tagName }}
</gl-link>
<span v-else v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span>
<span v-else v-gl-tooltip.bottom :title="__('Tag')">{{ release.tagName }}</span>
</div>
<release-block-milestones v-if="shouldRenderMilestones" :milestones="release.milestones" />
<div class="append-right-4">
&bull;
<span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)">
<span v-gl-tooltip.bottom :title="tooltipTitle(release.releasedAt)">
{{ releasedTimeAgo }}
</span>
</div>
......
......@@ -40,7 +40,7 @@ export default {
return Number.isNaN(percent) ? 0 : percent;
},
allIssueStats() {
return this.milestones.map(m => m.issue_stats || {});
return this.milestones.map(m => m.issueStats || {});
},
openIssuesCount() {
return this.allIssueStats.map(stats => stats.opened || 0).reduce(sumReducer);
......@@ -109,7 +109,7 @@ export default {
:key="milestone.id"
v-gl-tooltip
:title="milestone.description"
:href="milestone.web_url"
:href="milestone.webUrl"
class="append-right-4"
>
{{ milestone.title }}
......
......@@ -38,7 +38,7 @@ export default {
:key="milestone.id"
v-gl-tooltip
:title="milestone.description"
:href="milestone.web_url"
:href="milestone.webUrl"
class="mx-1 js-milestone-link"
>
{{ milestone.title }}
......
......@@ -22,8 +22,7 @@ export const fetchRelease = ({ dispatch, state }) => {
return api
.release(state.projectId, state.tagName)
.then(({ data: release }) => {
const camelCasedRelease = convertObjectPropsToCamelCase(release, { deep: true });
dispatch('receiveReleaseSuccess', camelCasedRelease);
dispatch('receiveReleaseSuccess', convertObjectPropsToCamelCase(release, { deep: true }));
})
.catch(error => {
dispatch('receiveReleaseError', error);
......
......@@ -2,7 +2,11 @@ import * as types from './mutation_types';
import createFlash from '~/flash';
import { __ } from '~/locale';
import api from '~/api';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import {
normalizeHeaders,
parseIntPagination,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
/**
* Commits a mutation to update the state while the main endpoint is being requested.
......@@ -28,7 +32,11 @@ export const fetchReleases = ({ dispatch }, { page = '1', projectId }) => {
export const receiveReleasesSuccess = ({ commit }, { data, headers }) => {
const pageInfo = parseIntPagination(normalizeHeaders(headers));
commit(types.RECEIVE_RELEASES_SUCCESS, { data, pageInfo });
const camelCasedReleases = convertObjectPropsToCamelCase(data, { deep: true });
commit(types.RECEIVE_RELEASES_SUCCESS, {
data: camelCasedReleases,
pageInfo,
});
};
export const receiveReleasesError = ({ commit }) => {
......
......@@ -13,7 +13,7 @@ describe('Release edit component', () => {
beforeEach(() => {
gon.api_version = 'v4';
releaseClone = JSON.parse(JSON.stringify(convertObjectPropsToCamelCase(release)));
releaseClone = convertObjectPropsToCamelCase(release, { deep: true });
state = {
release: releaseClone,
......
......@@ -2,12 +2,14 @@ import { mount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue';
import { release } from '../mock_data';
import { release as originalRelease } from '../mock_data';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
describe('Evidence Block', () => {
let wrapper;
let release;
const factory = (options = {}) => {
wrapper = mount(EvidenceBlock, {
......@@ -16,6 +18,8 @@ describe('Evidence Block', () => {
};
beforeEach(() => {
release = convertObjectPropsToCamelCase(originalRelease, { deep: true });
factory({
propsData: {
release,
......@@ -32,7 +36,7 @@ describe('Evidence Block', () => {
});
it('renders the title for the dowload link', () => {
expect(wrapper.find(GlLink).text()).toBe(`${release.tag_name}-evidence.json`);
expect(wrapper.find(GlLink).text()).toBe(`${release.tagName}-evidence.json`);
});
it('renders the correct hover text for the download', () => {
......@@ -40,19 +44,19 @@ describe('Evidence Block', () => {
});
it('renders the correct file link for download', () => {
expect(wrapper.find(GlLink).attributes().download).toBe(`${release.tag_name}-evidence.json`);
expect(wrapper.find(GlLink).attributes().download).toBe(`${release.tagName}-evidence.json`);
});
describe('sha text', () => {
it('renders the short sha initially', () => {
expect(wrapper.find('.js-short').text()).toBe(truncateSha(release.evidence_sha));
expect(wrapper.find('.js-short').text()).toBe(truncateSha(release.evidenceSha));
});
it('renders the long sha after expansion', () => {
wrapper.find('.js-text-expander-prepend').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find('.js-expanded').text()).toBe(release.evidence_sha);
expect(wrapper.find('.js-expanded').text()).toBe(release.evidenceSha);
});
});
});
......@@ -68,7 +72,7 @@ describe('Evidence Block', () => {
it('copies the sha', () => {
expect(wrapper.find(ClipboardButton).attributes('data-clipboard-text')).toBe(
release.evidence_sha,
release.evidenceSha,
);
});
});
......
......@@ -24,7 +24,7 @@ describe('Release block footer', () => {
const factory = (props = {}) => {
wrapper = mount(ReleaseBlockFooter, {
propsData: {
...convertObjectPropsToCamelCase(releaseClone),
...convertObjectPropsToCamelCase(releaseClone, { deep: true }),
...props,
},
});
......
import { shallowMount } from '@vue/test-utils';
import { cloneDeep, merge } from 'lodash';
import { merge } from 'lodash';
import { GlLink } from '@gitlab/ui';
import ReleaseBlockHeader from '~/releases/components/release_block_header.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
......@@ -18,9 +18,7 @@ describe('Release block header', () => {
};
beforeEach(() => {
release = convertObjectPropsToCamelCase(cloneDeep(originalRelease), {
ignoreKeyNames: ['_links'],
});
release = convertObjectPropsToCamelCase(originalRelease, { deep: true });
});
afterEach(() => {
......@@ -39,13 +37,13 @@ describe('Release block header', () => {
const link = findHeaderLink();
expect(link.text()).toBe(release.name);
expect(link.attributes('href')).toBe(release._links.self);
expect(link.attributes('href')).toBe(release.Links.self);
});
});
describe('when _links.self is missing', () => {
beforeEach(() => {
factory({ _links: { self: null } });
factory({ Links: { self: null } });
});
it('renders the title as text', () => {
......
......@@ -2,12 +2,13 @@ import { mount } from '@vue/test-utils';
import { GlProgressBar, GlLink, GlBadge, GlButton } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import ReleaseBlockMilestoneInfo from '~/releases/components/release_block_milestone_info.vue';
import { milestones } from '../mock_data';
import { milestones as originalMilestones } from '../mock_data';
import { MAX_MILESTONES_TO_DISPLAY } from '~/releases/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
describe('Release block milestone info', () => {
let wrapper;
let milestonesClone;
let milestones;
const factory = milestonesProp => {
wrapper = mount(ReleaseBlockMilestoneInfo, {
......@@ -20,7 +21,7 @@ describe('Release block milestone info', () => {
};
beforeEach(() => {
milestonesClone = JSON.parse(JSON.stringify(milestones));
milestones = convertObjectPropsToCamelCase(originalMilestones, { deep: true });
});
afterEach(() => {
......@@ -32,7 +33,7 @@ describe('Release block milestone info', () => {
const issuesContainer = () => wrapper.find('.js-issues-container');
describe('with default props', () => {
beforeEach(() => factory(milestonesClone));
beforeEach(() => factory(milestones));
it('renders the correct percentage', () => {
expect(milestoneProgressBarContainer().text()).toContain('41% complete');
......@@ -53,13 +54,13 @@ describe('Release block milestone info', () => {
it('renders a list of links to all associated milestones', () => {
expect(trimText(milestoneListContainer().text())).toContain('Milestones 13.6 • 13.5');
milestonesClone.forEach((m, i) => {
milestones.forEach((m, i) => {
const milestoneLink = milestoneListContainer()
.findAll(GlLink)
.at(i);
expect(milestoneLink.text()).toBe(m.title);
expect(milestoneLink.attributes('href')).toBe(m.web_url);
expect(milestoneLink.attributes('href')).toBe(m.webUrl);
expect(milestoneLink.attributes('title')).toBe(m.description);
});
});
......@@ -84,7 +85,7 @@ describe('Release block milestone info', () => {
beforeEach(() => {
lotsOfMilestones = [];
const template = milestonesClone[0];
const template = milestones[0];
for (let i = 0; i < MAX_MILESTONES_TO_DISPLAY + 10; i += 1) {
lotsOfMilestones.push({
......@@ -148,16 +149,16 @@ describe('Release block milestone info', () => {
/** Ensures we don't have any issues with dividing by zero when computing percentages */
describe('when all issue counts are zero', () => {
beforeEach(() => {
milestonesClone = milestonesClone.map(m => ({
milestones = milestones.map(m => ({
...m,
issue_stats: {
...m.issue_stats,
issueStats: {
...m.issueStats,
opened: 0,
closed: 0,
},
}));
return factory(milestonesClone);
return factory(milestones);
});
expectAllZeros();
......@@ -165,12 +166,12 @@ describe('Release block milestone info', () => {
describe('if the API response is missing the "issue_stats" property', () => {
beforeEach(() => {
milestonesClone = milestonesClone.map(m => ({
milestones = milestones.map(m => ({
...m,
issue_stats: undefined,
issueStats: undefined,
}));
return factory(milestonesClone);
return factory(milestones);
});
expectAllZeros();
......
......@@ -5,10 +5,12 @@ import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { release } from '../mock_data';
import { release as originalRelease } from '../mock_data';
import Icon from '~/vue_shared/components/icon.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
const { convertObjectPropsToCamelCase } = jest.requireActual('~/lib/utils/common_utils');
let mockLocationHash;
jest.mock('~/lib/utils/url_utility', () => ({
__esModule: true,
......@@ -22,7 +24,7 @@ jest.mock('~/lib/utils/common_utils', () => ({
describe('Release block', () => {
let wrapper;
let releaseClone;
let release;
const factory = (releaseProp, featureFlags = {}) => {
wrapper = mount(ReleaseBlock, {
......@@ -45,7 +47,7 @@ describe('Release block', () => {
beforeEach(() => {
jest.spyOn($.fn, 'renderGFM');
releaseClone = JSON.parse(JSON.stringify(release));
release = convertObjectPropsToCamelCase(originalRelease, { deep: true });
});
afterEach(() => {
......@@ -61,7 +63,7 @@ describe('Release block', () => {
it('renders an edit button that links to the "Edit release" page', () => {
expect(editButton().exists()).toBe(true);
expect(editButton().attributes('href')).toBe(release._links.edit_url);
expect(editButton().attributes('href')).toBe(release.Links.editUrl);
});
it('renders release name', () => {
......@@ -74,7 +76,7 @@ describe('Release block', () => {
});
it('renders release date', () => {
expect(wrapper.text()).toContain(timeagoMixin.methods.timeFormatted(release.released_at));
expect(wrapper.text()).toContain(timeagoMixin.methods.timeFormatted(release.releasedAt));
});
it('renders number of assets provided', () => {
......@@ -129,72 +131,72 @@ describe('Release block', () => {
});
it('renders commit sha', () => {
releaseClone.commit_path = '/commit/example';
release.commitPath = '/commit/example';
return factory(releaseClone).then(() => {
expect(wrapper.text()).toContain(release.commit.short_id);
return factory(release).then(() => {
expect(wrapper.text()).toContain(release.commit.shortId);
expect(wrapper.find('a[href="/commit/example"]').exists()).toBe(true);
});
});
it('renders tag name', () => {
releaseClone.tag_path = '/tag/example';
release.tagPath = '/tag/example';
return factory(releaseClone).then(() => {
expect(wrapper.text()).toContain(release.tag_name);
return factory(release).then(() => {
expect(wrapper.text()).toContain(release.tagName);
expect(wrapper.find('a[href="/tag/example"]').exists()).toBe(true);
});
});
it("does not render an edit button if release._links.edit_url isn't a string", () => {
delete releaseClone._links;
it("does not render an edit button if release.Links.editUrl isn't a string", () => {
delete release.Links;
return factory(releaseClone).then(() => {
return factory(release).then(() => {
expect(editButton().exists()).toBe(false);
});
});
it('does not render the milestone list if no milestones are associated to the release', () => {
delete releaseClone.milestones;
delete release.milestones;
return factory(releaseClone).then(() => {
return factory(release).then(() => {
expect(milestoneListLabel().exists()).toBe(false);
});
});
it('renders upcoming release badge', () => {
releaseClone.upcoming_release = true;
release.upcomingRelease = true;
return factory(releaseClone).then(() => {
return factory(release).then(() => {
expect(wrapper.text()).toContain('Upcoming Release');
});
});
it('slugifies the tag_name before setting it as the elements ID', () => {
releaseClone.tag_name = 'a dangerous tag name <script>alert("hello")</script>';
it('slugifies the tagName before setting it as the elements ID', () => {
release.tagName = 'a dangerous tag name <script>alert("hello")</script>';
return factory(releaseClone).then(() => {
return factory(release).then(() => {
expect(wrapper.attributes().id).toBe('a-dangerous-tag-name-script-alert-hello-script');
});
});
describe('evidence block', () => {
it('renders the evidence block when the evidence is available and the feature flag is true', () =>
factory(releaseClone, { releaseEvidenceCollection: true }).then(() =>
factory(release, { releaseEvidenceCollection: true }).then(() =>
expect(wrapper.find(EvidenceBlock).exists()).toBe(true),
));
it('does not render the evidence block when the evidence is available but the feature flag is false', () =>
factory(releaseClone, { releaseEvidenceCollection: true }).then(() =>
factory(release, { releaseEvidenceCollection: true }).then(() =>
expect(wrapper.find(EvidenceBlock).exists()).toBe(true),
));
it('does not render the evidence block when there is no evidence', () => {
releaseClone.evidence_sha = null;
release.evidenceSha = null;
return factory(releaseClone).then(() => {
return factory(release).then(() => {
expect(wrapper.find(EvidenceBlock).exists()).toBe(false);
});
});
......@@ -222,7 +224,7 @@ describe('Release block', () => {
});
it("attempts to scroll itself into view if the anchor tag matches the release's tag name", () => {
mockLocationHash = release.tag_name;
mockLocationHash = release.tagName;
return factory(release).then(() => {
expect(scrollToElement).toHaveBeenCalledTimes(1);
......@@ -231,7 +233,7 @@ describe('Release block', () => {
});
it('renders with a light blue background if it is the target of the anchor', () => {
mockLocationHash = release.tag_name;
mockLocationHash = release.tagName;
return factory(release).then(() => {
expect(hasTargetBlueBackground()).toBe(true);
......@@ -275,16 +277,16 @@ describe('Release block', () => {
expect(milestoneLink.text()).toBe(milestone.title);
expect(milestoneLink.attributes('href')).toBe(milestone.web_url);
expect(milestoneLink.attributes('href')).toBe(milestone.webUrl);
expect(milestoneLink.attributes('title')).toBe(milestone.description);
});
});
it('renders the label as "Milestone" if only a single milestone is passed in', () => {
releaseClone.milestones = releaseClone.milestones.slice(0, 1);
release.milestones = release.milestones.slice(0, 1);
return factory(releaseClone, { releaseIssueSummary: false }).then(() => {
return factory(release, { releaseIssueSummary: false }).then(() => {
expect(
milestoneListLabel()
.find('.js-label-text')
......
......@@ -12,6 +12,7 @@ import {
release,
releases,
} from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
describe('Releases App ', () => {
const Component = Vue.extend(app);
......@@ -27,7 +28,10 @@ describe('Releases App ', () => {
beforeEach(() => {
store = createStore({ list: listModule });
releasesPagination = _.range(21).map(index => ({ ...release, tag_name: `${index}.00` }));
releasesPagination = _.range(21).map(index => ({
...convertObjectPropsToCamelCase(release, { deep: true }),
tagName: `${index}.00`,
}));
});
afterEach(() => {
......
......@@ -8,16 +8,18 @@ import {
import state from '~/releases/stores/modules/list/state';
import * as types from '~/releases/stores/modules/list/mutation_types';
import api from '~/api';
import { parseIntPagination } from '~/lib/utils/common_utils';
import { pageInfoHeadersWithoutPagination, releases } from '../../../mock_data';
import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { pageInfoHeadersWithoutPagination, releases as originalReleases } from '../../../mock_data';
describe('Releases State actions', () => {
let mockedState;
let pageInfo;
let releases;
beforeEach(() => {
mockedState = state();
pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
releases = convertObjectPropsToCamelCase(originalReleases, { deep: true });
});
describe('requestReleases', () => {
......
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