diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-19 11:27:35 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-19 11:27:35 +0300 |
commit | 7e9c479f7de77702622631cff2628a9c8dcbc627 (patch) | |
tree | c8f718a08e110ad7e1894510980d2155a6549197 /app/assets/javascripts/releases | |
parent | e852b0ae16db4052c1c567d9efa4facc81146e88 (diff) |
Add latest changes from gitlab-org/gitlab@13-6-stable-eev13.6.0-rc42
Diffstat (limited to 'app/assets/javascripts/releases')
16 files changed, 336 insertions, 102 deletions
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue index 1a07e0ed762..8d1bc44cba0 100644 --- a/app/assets/javascripts/releases/components/app_edit_new.vue +++ b/app/assets/javascripts/releases/components/app_edit_new.vue @@ -6,7 +6,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { BACK_URL_PARAM } from '~/releases/constants'; import { getParameterByName } from '~/lib/utils/common_utils'; import AssetLinksForm from './asset_links_form.vue'; -import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue'; +import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue'; import TagField from './tag_field.vue'; export default { @@ -29,11 +29,12 @@ export default { 'markdownDocsPath', 'markdownPreviewPath', 'releasesPagePath', - 'updateReleaseApiDocsPath', 'release', 'newMilestonePath', 'manageMilestonesPath', 'projectId', + 'groupId', + 'groupMilestonesAvailable', ]), ...mapGetters('detail', ['isValid', 'isExistingRelease']), showForm() { @@ -141,6 +142,8 @@ export default { <milestone-combobox v-model="releaseMilestones" :project-id="projectId" + :group-id="groupId" + :group-milestones-available="groupMilestonesAvailable" :extra-links="milestoneComboboxExtraLinks" /> </div> diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue index 422d8bf630d..5064b7dd6ad 100644 --- a/app/assets/javascripts/releases/components/app_index.vue +++ b/app/assets/javascripts/releases/components/app_index.vue @@ -6,6 +6,7 @@ import { __ } from '~/locale'; import ReleaseBlock from './release_block.vue'; import ReleasesPagination from './releases_pagination.vue'; import ReleaseSkeletonLoader from './release_skeleton_loader.vue'; +import ReleasesSort from './releases_sort.vue'; export default { name: 'ReleasesApp', @@ -16,6 +17,7 @@ export default { ReleaseBlock, ReleasesPagination, ReleaseSkeletonLoader, + ReleasesSort, }, computed: { ...mapState('list', [ @@ -62,16 +64,20 @@ export default { </script> <template> <div class="flex flex-column mt-2"> - <gl-button - v-if="newReleasePath" - :href="newReleasePath" - :aria-describedby="shouldRenderEmptyState && 'releases-description'" - category="primary" - variant="success" - class="align-self-end mb-2 js-new-release-btn" - > - {{ __('New release') }} - </gl-button> + <div class="gl-align-self-end gl-mb-3"> + <releases-sort class="gl-mr-2" @sort:changed="fetchReleases" /> + + <gl-button + v-if="newReleasePath" + :href="newReleasePath" + :aria-describedby="shouldRenderEmptyState && 'releases-description'" + category="primary" + variant="success" + class="js-new-release-btn" + > + {{ __('New release') }} + </gl-button> + </div> <release-skeleton-loader v-if="isLoading" class="js-loading" /> diff --git a/app/assets/javascripts/releases/components/issuable_stats.vue b/app/assets/javascripts/releases/components/issuable_stats.vue new file mode 100644 index 00000000000..d005d8e10dd --- /dev/null +++ b/app/assets/javascripts/releases/components/issuable_stats.vue @@ -0,0 +1,97 @@ +<script> +import { GlLink, GlBadge, GlSprintf } from '@gitlab/ui'; + +export default { + name: 'IssuableStats', + components: { + GlLink, + GlBadge, + GlSprintf, + }, + props: { + label: { + type: String, + required: true, + }, + total: { + type: Number, + required: true, + }, + closed: { + type: Number, + required: true, + }, + merged: { + type: Number, + required: false, + default: null, + }, + openedPath: { + type: String, + required: false, + default: '', + }, + closedPath: { + type: String, + required: false, + default: '', + }, + mergedPath: { + type: String, + required: false, + default: '', + }, + }, + computed: { + opened() { + return this.total - (this.closed + (this.merged || 0)); + }, + showMerged() { + return this.merged != null; + }, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-flex-direction-column gl-flex-shrink-0 gl-mr-6 gl-mb-5"> + <span class="gl-mb-2"> + {{ label }} + <gl-badge variant="muted" size="sm">{{ total }}</gl-badge> + </span> + <div class="gl-display-flex"> + <span class="gl-white-space-pre-wrap" data-testid="open-stat"> + <gl-sprintf :message="__('Open: %{open}')"> + <template #open> + <gl-link v-if="openedPath" :href="openedPath">{{ opened }}</gl-link> + <template v-else>{{ opened }}</template> + </template> + </gl-sprintf> + </span> + + <template v-if="showMerged"> + <span class="gl-mx-2">•</span> + + <span class="gl-white-space-pre-wrap" data-testid="merged-stat"> + <gl-sprintf :message="__('Merged: %{merged}')"> + <template #merged> + <gl-link v-if="mergedPath" :href="mergedPath">{{ merged }}</gl-link> + <template v-else>{{ merged }}</template> + </template> + </gl-sprintf> + </span> + </template> + + <span class="gl-mx-2">•</span> + + <span class="gl-white-space-pre-wrap" data-testid="closed-stat"> + <gl-sprintf :message="__('Closed: %{closed}')"> + <template #closed> + <gl-link v-if="closedPath" :href="closedPath">{{ closed }}</gl-link> + <template v-else>{{ closed }}</template> + </template> + </gl-sprintf> + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue index e9163a52792..b89e5f2df3f 100644 --- a/app/assets/javascripts/releases/components/release_block.vue +++ b/app/assets/javascripts/releases/components/release_block.vue @@ -87,9 +87,14 @@ export default { <release-block-header :release="release" /> <div class="card-body"> <div v-if="shouldRenderMilestoneInfo"> + <!-- TODO: Switch open* links to opened* once fields have been updated in GraphQL --> <release-block-milestone-info :milestones="milestones" - :open-issues-path="release._links.issuesUrl" + :opened-issues-path="release._links.openedIssuesUrl" + :closed-issues-path="release._links.closedIssuesUrl" + :opened-merge-requests-path="release._links.openedMergeRequestsUrl" + :merged-merge-requests-path="release._links.mergedMergeRequestsUrl" + :closed-merge-requests-path="release._links.closedMergeRequestsUrl" /> <hr class="mb-3 mt-0" /> </div> diff --git a/app/assets/javascripts/releases/components/release_block_milestone_info.vue b/app/assets/javascripts/releases/components/release_block_milestone_info.vue index deff673cc17..daa9c3480f4 100644 --- a/app/assets/javascripts/releases/components/release_block_milestone_info.vue +++ b/app/assets/javascripts/releases/components/release_block_milestone_info.vue @@ -1,24 +1,16 @@ <script> -import { - GlProgressBar, - GlLink, - GlBadge, - GlButton, - GlTooltipDirective, - GlSprintf, -} from '@gitlab/ui'; -import { sum } from 'lodash'; +import { GlProgressBar, GlLink, GlButton, GlTooltipDirective } from '@gitlab/ui'; import { __, n__, sprintf } from '~/locale'; import { MAX_MILESTONES_TO_DISPLAY } from '../constants'; +import IssuableStats from './issuable_stats.vue'; export default { name: 'ReleaseBlockMilestoneInfo', components: { GlProgressBar, GlLink, - GlBadge, GlButton, - GlSprintf, + IssuableStats, }, directives: { GlTooltip: GlTooltipDirective, @@ -28,7 +20,7 @@ export default { type: Array, required: true, }, - openIssuesPath: { + openedIssuesPath: { type: String, required: false, default: '', @@ -38,6 +30,21 @@ export default { required: false, default: '', }, + openedMergeRequestsPath: { + type: String, + required: false, + default: '', + }, + mergedMergeRequestsPath: { + type: String, + required: false, + default: '', + }, + closedMergeRequestsPath: { + type: String, + required: false, + default: '', + }, }, data() { return { @@ -52,30 +59,49 @@ export default { }); }, percentComplete() { - const percent = Math.round((this.closedIssuesCount / this.totalIssuesCount) * 100); + const percent = Math.round((this.issueCounts.closed / this.issueCounts.total) * 100); return Number.isNaN(percent) ? 0 : percent; }, - allIssueStats() { - return this.milestones.map(m => m.issueStats || {}); - }, - totalIssuesCount() { - return sum(this.allIssueStats.map(stats => stats.total || 0)); - }, - closedIssuesCount() { - return sum(this.allIssueStats.map(stats => stats.closed || 0)); - }, - openIssuesCount() { - return this.totalIssuesCount - this.closedIssuesCount; + issueCounts() { + return this.milestones + .map(m => m.issueStats || {}) + .reduce( + (acc, current) => { + acc.total += current.total || 0; + acc.closed += current.closed || 0; + + return acc; + }, + { + total: 0, + closed: 0, + }, + ); + }, + showMergeRequestStats() { + return this.milestones.some(m => m.mrStats); + }, + mergeRequestCounts() { + return this.milestones + .map(m => m.mrStats || {}) + .reduce( + (acc, current) => { + acc.total += current.total || 0; + acc.merged += current.merged || 0; + acc.closed += current.closed || 0; + + return acc; + }, + { + total: 0, + merged: 0, + closed: 0, + }, + ); }, milestoneLabelText() { return n__('Milestone', 'Milestones', this.milestones.length); }, - issueCountsText() { - return sprintf(__('Open: %{open} • Closed: %{closed}'), { - open: this.openIssuesCount, - closed: this.closedIssuesCount, - }); - }, milestonesToDisplay() { return this.showAllMilestones ? this.milestones @@ -106,20 +132,22 @@ export default { }; </script> <template> - <div class="release-block-milestone-info d-flex align-items-start flex-wrap"> + <div class="release-block-milestone-info gl-display-flex gl-flex-wrap"> <div v-gl-tooltip - class="milestone-progress-bar-container js-milestone-progress-bar-container d-flex flex-column align-items-start flex-shrink-1 mr-4 mb-3" + class="milestone-progress-bar-container js-milestone-progress-bar-container gl-display-flex gl-flex-direction-column gl-mr-6 gl-mb-5" :title="__('Closed issues')" > - <span class="mb-2">{{ percentCompleteText }}</span> - <span class="w-100"> - <gl-progress-bar :value="closedIssuesCount" :max="totalIssuesCount" variant="success" /> + <span class="gl-mb-3">{{ percentCompleteText }}</span> + <span class="gl-w-full"> + <gl-progress-bar :value="issueCounts.closed" :max="issueCounts.total" variant="success" /> </span> </div> - <div class="d-flex flex-column align-items-start mr-4 mb-3 js-milestone-list-container"> - <span class="mb-1">{{ milestoneLabelText }}</span> - <div class="d-flex flex-wrap align-items-end"> + <div + class="gl-display-flex gl-flex-direction-column gl-mr-6 gl-mb-5 js-milestone-list-container" + > + <span class="gl-mb-2">{{ milestoneLabelText }}</span> + <div class="gl-display-flex gl-flex-wrap gl-align-items-flex-end"> <template v-for="(milestone, index) in milestonesToDisplay"> <gl-link :key="milestone.id" @@ -141,32 +169,24 @@ export default { </template> </div> </div> - <div class="d-flex flex-column align-items-start flex-shrink-0 mr-4 mb-3 js-issues-container"> - <span class="mb-1"> - {{ __('Issues') }} - <gl-badge variant="muted" size="sm">{{ totalIssuesCount }}</gl-badge> - </span> - <div class="d-flex"> - <gl-link v-if="openIssuesPath" ref="openIssuesLink" :href="openIssuesPath"> - <gl-sprintf :message="__('Open: %{openIssuesCount}')"> - <template #openIssuesCount>{{ openIssuesCount }}</template> - </gl-sprintf> - </gl-link> - <span v-else ref="openIssuesText"> - {{ sprintf(__('Open: %{openIssuesCount}'), { openIssuesCount }) }} - </span> - - <span class="mx-1">•</span> - - <gl-link v-if="closedIssuesPath" ref="closedIssuesLink" :href="closedIssuesPath"> - <gl-sprintf :message="__('Closed: %{closedIssuesCount}')"> - <template #closedIssuesCount>{{ closedIssuesCount }}</template> - </gl-sprintf> - </gl-link> - <span v-else ref="closedIssuesText"> - {{ sprintf(__('Closed: %{closedIssuesCount}'), { closedIssuesCount }) }} - </span> - </div> - </div> + <issuable-stats + :label="__('Issues')" + :total="issueCounts.total" + :closed="issueCounts.closed" + :opened-path="openedIssuesPath" + :closed-path="closedIssuesPath" + data-testid="issue-stats" + /> + <issuable-stats + v-if="showMergeRequestStats" + :label="__('Merge Requests')" + :total="mergeRequestCounts.total" + :merged="mergeRequestCounts.merged" + :closed="mergeRequestCounts.closed" + :opened-path="openedMergeRequestsPath" + :merged-path="mergedMergeRequestsPath" + :closed-path="closedMergeRequestsPath" + data-testid="merge-request-stats" + /> </div> </template> diff --git a/app/assets/javascripts/releases/components/releases_sort.vue b/app/assets/javascripts/releases/components/releases_sort.vue new file mode 100644 index 00000000000..50f6f3c19bd --- /dev/null +++ b/app/assets/javascripts/releases/components/releases_sort.vue @@ -0,0 +1,62 @@ +<script> +import { GlSorting, GlSortingItem } from '@gitlab/ui'; +import { mapState, mapActions } from 'vuex'; +import { ASCENDING_ODER, DESCENDING_ORDER, SORT_OPTIONS } from '../constants'; + +export default { + name: 'ReleasesSort', + components: { + GlSorting, + GlSortingItem, + }, + computed: { + ...mapState('list', { + orderBy: state => state.sorting.orderBy, + sort: state => state.sorting.sort, + }), + sortOptions() { + return SORT_OPTIONS; + }, + sortText() { + const option = this.sortOptions.find(s => s.orderBy === this.orderBy); + return option.label; + }, + isSortAscending() { + return this.sort === ASCENDING_ODER; + }, + }, + methods: { + ...mapActions('list', ['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'); + }, + isActiveSortItem(item) { + return this.orderBy === item; + }, + }, +}; +</script> + +<template> + <gl-sorting + :text="sortText" + :is-ascending="isSortAscending" + data-testid="releases-sort" + @sortDirectionChange="onDirectionChange" + > + <gl-sorting-item + v-for="item in sortOptions" + :key="item.orderBy" + :active="isActiveSortItem(item.orderBy)" + @click="onSortItemClick(item.orderBy)" + > + {{ item.label }} + </gl-sorting-item> + </gl-sorting> +</template> diff --git a/app/assets/javascripts/releases/components/tag_field_existing.vue b/app/assets/javascripts/releases/components/tag_field_existing.vue index b84e713df26..046885fe2f6 100644 --- a/app/assets/javascripts/releases/components/tag_field_existing.vue +++ b/app/assets/javascripts/releases/components/tag_field_existing.vue @@ -1,14 +1,14 @@ <script> import { mapState } from 'vuex'; import { uniqueId } from 'lodash'; -import { GlFormGroup, GlFormInput, GlLink, GlSprintf } from '@gitlab/ui'; +import { GlFormGroup, GlFormInput } from '@gitlab/ui'; import FormFieldContainer from './form_field_container.vue'; export default { name: 'TagFieldExisting', - components: { GlFormGroup, GlFormInput, GlSprintf, GlLink, FormFieldContainer }, + components: { GlFormGroup, GlFormInput, FormFieldContainer }, computed: { - ...mapState('detail', ['release', 'updateReleaseApiDocsPath']), + ...mapState('detail', ['release']), inputId() { return uniqueId('tag-name-input-'); }, @@ -32,19 +32,7 @@ export default { </form-field-container> <template #description> <div :id="helpId" data-testid="tag-name-help"> - <gl-sprintf - :message=" - __( - 'Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}', - ) - " - > - <template #link="{ content }"> - <gl-link :href="updateReleaseApiDocsPath" target="_blank"> - {{ content }} - </gl-link> - </template> - </gl-sprintf> + {{ __("The tag name can't be changed for an existing release.") }} </div> </template> </gl-form-group> diff --git a/app/assets/javascripts/releases/constants.js b/app/assets/javascripts/releases/constants.js index 953e7b4189c..8979aa1394d 100644 --- a/app/assets/javascripts/releases/constants.js +++ b/app/assets/javascripts/releases/constants.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + export const MAX_MILESTONES_TO_DISPLAY = 5; export const BACK_URL_PARAM = 'back_url'; @@ -12,3 +14,19 @@ export const ASSET_LINK_TYPE = Object.freeze({ export const DEFAULT_ASSET_LINK_TYPE = ASSET_LINK_TYPE.OTHER; export const PAGE_SIZE = 20; + +export const ASCENDING_ODER = 'asc'; +export const DESCENDING_ORDER = 'desc'; +export const RELEASED_AT = 'released_at'; +export const CREATED_AT = 'created_at'; + +export const SORT_OPTIONS = [ + { + orderBy: RELEASED_AT, + label: __('Released date'), + }, + { + orderBy: CREATED_AT, + label: __('Created date'), + }, +]; diff --git a/app/assets/javascripts/releases/queries/all_releases.query.graphql b/app/assets/javascripts/releases/queries/all_releases.query.graphql index c35306f163d..a07dabb9fd6 100644 --- a/app/assets/javascripts/releases/queries/all_releases.query.graphql +++ b/app/assets/javascripts/releases/queries/all_releases.query.graphql @@ -1,8 +1,15 @@ #import "./release.fragment.graphql" -query allReleases($fullPath: ID!, $first: Int, $last: Int, $before: String, $after: String) { +query allReleases( + $fullPath: ID! + $first: Int + $last: Int + $before: String + $after: String + $sort: ReleaseSort +) { project(fullPath: $fullPath) { - releases(first: $first, last: $last, before: $before, after: $after) { + releases(first: $first, last: $last, before: $before, after: $after, sort: $sort) { nodes { ...Release } diff --git a/app/assets/javascripts/releases/queries/release.fragment.graphql b/app/assets/javascripts/releases/queries/release.fragment.graphql index 445ed616348..3a742db7d9e 100644 --- a/app/assets/javascripts/releases/queries/release.fragment.graphql +++ b/app/assets/javascripts/releases/queries/release.fragment.graphql @@ -4,6 +4,7 @@ fragment Release on Release { tagPath descriptionHtml releasedAt + createdAt upcomingRelease assets { count @@ -33,9 +34,12 @@ fragment Release on Release { } links { editUrl - issuesUrl - mergeRequestsUrl selfUrl + openedIssuesUrl + closedIssuesUrl + openedMergeRequestsUrl + mergedMergeRequestsUrl + closedMergeRequestsUrl } commit { sha diff --git a/app/assets/javascripts/releases/stores/modules/detail/state.js b/app/assets/javascripts/releases/stores/modules/detail/state.js index 782a5c46d6c..315d07ac664 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/state.js +++ b/app/assets/javascripts/releases/stores/modules/detail/state.js @@ -1,9 +1,10 @@ export default ({ projectId, + groupId, + groupMilestonesAvailable = false, projectPath, markdownDocsPath, markdownPreviewPath, - updateReleaseApiDocsPath, releaseAssetsDocsPath, manageMilestonesPath, newMilestonePath, @@ -13,10 +14,11 @@ export default ({ defaultBranch = null, }) => ({ projectId, + groupId, + groupMilestonesAvailable: Boolean(groupMilestonesAvailable), projectPath, markdownDocsPath, markdownPreviewPath, - updateReleaseApiDocsPath, releaseAssetsDocsPath, manageMilestonesPath, newMilestonePath, diff --git a/app/assets/javascripts/releases/stores/modules/list/actions.js b/app/assets/javascripts/releases/stores/modules/list/actions.js index 02e67415e63..a62f7c25464 100644 --- a/app/assets/javascripts/releases/stores/modules/list/actions.js +++ b/app/assets/javascripts/releases/stores/modules/list/actions.js @@ -42,6 +42,10 @@ export const fetchReleasesGraphQl = ( ) => { commit(types.REQUEST_RELEASES); + const { sort, orderBy } = state.sorting; + const orderByParam = orderBy === 'created_at' ? 'created' : orderBy; + const sortParams = `${orderByParam}_${sort}`.toUpperCase(); + let paginationParams; if (!before && !after) { paginationParams = { first: PAGE_SIZE }; @@ -60,6 +64,7 @@ export const fetchReleasesGraphQl = ( query: allReleasesQuery, variables: { fullPath: state.projectPath, + sort: sortParams, ...paginationParams, }, }) @@ -80,8 +85,10 @@ export const fetchReleasesGraphQl = ( export const fetchReleasesRest = ({ dispatch, commit, state }, { page }) => { commit(types.REQUEST_RELEASES); + const { sort, orderBy } = state.sorting; + api - .releases(state.projectId, { page }) + .releases(state.projectId, { page, sort, order_by: orderBy }) .then(({ data, headers }) => { const restPageInfo = parseIntPagination(normalizeHeaders(headers)); const camelCasedReleases = convertObjectPropsToCamelCase(data, { deep: true }); @@ -98,3 +105,5 @@ export const receiveReleasesError = ({ commit }) => { commit(types.RECEIVE_RELEASES_ERROR); createFlash(__('An error occurred while fetching the releases. Please try again.')); }; + +export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data); diff --git a/app/assets/javascripts/releases/stores/modules/list/mutation_types.js b/app/assets/javascripts/releases/stores/modules/list/mutation_types.js index a74bf15c515..669168efb88 100644 --- a/app/assets/javascripts/releases/stores/modules/list/mutation_types.js +++ b/app/assets/javascripts/releases/stores/modules/list/mutation_types.js @@ -1,3 +1,4 @@ export const REQUEST_RELEASES = 'REQUEST_RELEASES'; export const RECEIVE_RELEASES_SUCCESS = 'RECEIVE_RELEASES_SUCCESS'; export const RECEIVE_RELEASES_ERROR = 'RECEIVE_RELEASES_ERROR'; +export const SET_SORTING = 'SET_SORTING'; diff --git a/app/assets/javascripts/releases/stores/modules/list/mutations.js b/app/assets/javascripts/releases/stores/modules/list/mutations.js index 296487cfee2..e1aaa2e2a19 100644 --- a/app/assets/javascripts/releases/stores/modules/list/mutations.js +++ b/app/assets/javascripts/releases/stores/modules/list/mutations.js @@ -39,4 +39,8 @@ export default { state.restPageInfo = {}; state.graphQlPageInfo = {}; }, + + [types.SET_SORTING](state, sorting) { + state.sorting = { ...state.sorting, ...sorting }; + }, }; diff --git a/app/assets/javascripts/releases/stores/modules/list/state.js b/app/assets/javascripts/releases/stores/modules/list/state.js index 0bffaa0f9db..164a496d450 100644 --- a/app/assets/javascripts/releases/stores/modules/list/state.js +++ b/app/assets/javascripts/releases/stores/modules/list/state.js @@ -1,3 +1,5 @@ +import { DESCENDING_ORDER, RELEASED_AT } from '../../../constants'; + export default ({ projectId, projectPath, @@ -16,4 +18,8 @@ export default ({ releases: [], restPageInfo: {}, graphQlPageInfo: {}, + sorting: { + sort: DESCENDING_ORDER, + orderBy: RELEASED_AT, + }, }); diff --git a/app/assets/javascripts/releases/util.js b/app/assets/javascripts/releases/util.js index 445c429fd96..464f0594b8d 100644 --- a/app/assets/javascripts/releases/util.js +++ b/app/assets/javascripts/releases/util.js @@ -15,7 +15,9 @@ import { export const releaseToApiJson = (release, createFrom = null) => { const name = release.name?.trim().length > 0 ? release.name.trim() : null; - const milestones = release.milestones ? release.milestones.map(milestone => milestone.title) : []; + // Milestones may be either a list of milestone objects OR just a list + // of milestone titles. The API requires only the titles be sent. + const milestones = (release.milestones || []).map(m => m.title || m); return convertObjectPropsToSnakeCase( { |