Skip to content

Commit 47e6572

Browse files
committed
fix(FeedVersionNavigator): reverse version sort; add retrieval method filtering
re #544
1 parent 3d16208 commit 47e6572

File tree

12 files changed

+255
-101
lines changed

12 files changed

+255
-101
lines changed

lib/common/actions/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function createVoidPayloadAction (type: string) {
1111
return () => ({ type })
1212
}
1313

14-
export function secureFetch (url: string, method: string = 'get', payload: any, raw: boolean = false, isJSON: boolean = true, actionOnFail: any): any {
14+
export function secureFetch (url: string, method: string = 'get', payload?: any, raw: boolean = false, isJSON: boolean = true, actionOnFail?: string): any {
1515
return function (dispatch: dispatchFn, getState: getStateFn) {
1616
function consoleError (message) {
1717
console.error(`Error making ${method} request to ${url}: `, message)
@@ -95,7 +95,7 @@ export function fetchGraphQL ({
9595
}: {
9696
errorMessage?: string,
9797
query: string,
98-
variables: any
98+
variables?: {[key: string]: string | number | Array<string>}
9999
}): any {
100100
return function (dispatch: dispatchFn, getState: getStateFn) {
101101
const body = {

lib/editor/actions/trip.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export function fetchTripsForCalendar (
9292
) {
9393
return function (dispatch: dispatchFn, getState: getStateFn) {
9494
const namespace = getEditorNamespace(feedId, getState())
95+
if (!namespace) throw new Error('Editor namespace is undefined!')
9596
// This fetches patterns on the pattern_id field (rather than ID) because
9697
// pattern_id is needed to join on the nested trips table
9798
const query = `query ($namespace: String, $pattern_id: [String], $service_id: [String]) {
@@ -277,6 +278,7 @@ export function fetchCalendarTripCountsForPattern (
277278
) {
278279
return function (dispatch: dispatchFn, getState: getStateFn) {
279280
const namespace = getEditorNamespace(feedId, getState())
281+
if (!namespace) throw new Error('Editor namespace is undefined!')
280282
// This fetches patterns on the pattern_id field (rather than ID) because
281283
// pattern_id is needed to join on the nested trips table
282284
const query = `query ($namespace: String, $pattern_id: String) {
@@ -304,6 +306,7 @@ export function fetchCalendarTripCountsForPattern (
304306
export function fetchTripCounts (feedId: string) {
305307
return function (dispatch: dispatchFn, getState: getStateFn) {
306308
const namespace = getEditorNamespace(feedId, getState())
309+
if (!namespace) throw new Error('Editor namespace is undefined!')
307310
// This fetches patterns on the pattern_id field (rather than ID) because
308311
// pattern_id is needed to join on the nested trips table
309312
const query = `query ($namespace: String) {

lib/manager/actions/status.js

Lines changed: 61 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import {API_PREFIX} from '../../common/constants'
88
import {isExtensionEnabled} from '../../common/util/config'
99
import { fetchDeployment } from './deployments'
1010
import { fetchFeedSource } from './feeds'
11-
import { downloadMergedFeedViaToken } from './projects'
11+
import { downloadMergedFeedViaToken, fetchProjectWithFeeds } from './projects'
1212
import { downloadSnapshotViaCredentials } from '../../editor/actions/snapshots'
1313

14-
import type {DataToolsConfig, ServerJob} from '../../types'
14+
import type {DataToolsConfig, MergeFeedsResult, ServerJob} from '../../types'
1515
import type {dispatchFn, getStateFn} from '../../types/reducers'
1616

1717
type ErrorMessage = {
@@ -21,6 +21,13 @@ type ErrorMessage = {
2121
title?: string
2222
}
2323

24+
type ModalContent = {
25+
action?: any,
26+
body: string,
27+
detail?: any,
28+
title: string
29+
}
30+
2431
export const clearStatusModal = createVoidPayloadAction('CLEAR_STATUS_MODAL')
2532
const handlingFinishedJob = createAction(
2633
'HANDLING_FINISHED_JOB',
@@ -57,12 +64,7 @@ const setAppInfo = createAction(
5764

5865
const setStatusModal = createAction(
5966
'SET_STATUS_MODAL',
60-
(payload: {
61-
action?: any,
62-
body: string,
63-
detail?: any,
64-
title: string
65-
}) => payload
67+
(payload: ModalContent) => payload
6668
)
6769

6870
export type StatusActions = ActionType<typeof clearStatusModal> |
@@ -134,6 +136,51 @@ export async function fetchAppInfo () {
134136
}
135137
}
136138

139+
function getMergeFeedModalContent (result: MergeFeedsResult): ModalContent {
140+
const details = []
141+
// Do nothing or show merged feed modal? Feed version is be created
142+
details.push('Remapped ID count: ' + result.remappedReferences)
143+
if (Object.keys(result.remappedIds).length > 0) {
144+
const remappedIdStrings = []
145+
for (let key in result.remappedIds) {
146+
// Modify key to remove feed name.
147+
const split = key.split(':')
148+
const tableAndId = split.splice(1, 1)
149+
remappedIdStrings.push(`${tableAndId.join(':')} -> ${result.remappedIds[key]}`)
150+
}
151+
details.push('Remapped IDs: ' + remappedIdStrings.join(', '))
152+
}
153+
if (result.skippedIds.length > 0) {
154+
const skippedRecordsForTables = {}
155+
result.skippedIds.forEach(id => {
156+
const table = id.split(':')[0]
157+
if (skippedRecordsForTables[table]) {
158+
skippedRecordsForTables[table] = skippedRecordsForTables[table] + 1
159+
} else {
160+
skippedRecordsForTables[table] = 1
161+
}
162+
})
163+
const skippedRecordsStrings = []
164+
for (let key in skippedRecordsForTables) {
165+
skippedRecordsStrings.push(`${key} - ${skippedRecordsForTables[key]}`)
166+
}
167+
details.push('Skipped records: ' + skippedRecordsStrings.join(', '))
168+
}
169+
if (result.idConflicts.length > 0) {
170+
// const conflicts = result.idConflicts
171+
details.push('ID conflicts: ' + result.idConflicts.join(', '))
172+
}
173+
return {
174+
title: result.failed
175+
? 'Warning: Errors encountered during feed merge!'
176+
: 'Feed merge was successful!',
177+
body: result.failed
178+
? `Merge failed with ${result.errorCount} errors. ${result.failureReasons.join(', ')}`
179+
: `Merge was completed successfully. A new version will be processed/validated containing the resulting feed.`,
180+
detail: details.join('\n')
181+
}
182+
}
183+
137184
/* eslint-disable complexity */
138185
export function handleFinishedJob (job: ServerJob) {
139186
return function (dispatch: dispatchFn, getState: getStateFn) {
@@ -203,60 +250,21 @@ export function handleFinishedJob (job: ServerJob) {
203250
// If merging feeds for the project, end result is to download zip.
204251
if (!job.projectId) {
205252
// FIXME use setErrorMessage instead?
206-
console.warn('No project found on job')
207-
return
253+
throw new Error('No project found on job')
208254
}
209-
dispatch(downloadMergedFeedViaToken(job.projectId, false))
255+
// FIXME
256+
dispatch(fetchProjectWithFeeds(job.projectId))
210257
} else {
211258
const result = job.mergeFeedsResult
212-
const details = []
213259
if (result) {
214-
// Do nothing or show merged feed modal? Feed version is be created
215-
details.push('Remapped ID count: ' + result.remappedReferences)
216-
if (Object.keys(result.remappedIds).length > 0) {
217-
const remappedIdStrings = []
218-
for (let key in result.remappedIds) {
219-
// Modify key to remove feed name.
220-
const split = key.split(':')
221-
const tableAndId = split.splice(1, 1)
222-
remappedIdStrings.push(`${tableAndId.join(':')} -> ${result.remappedIds[key]}`)
223-
}
224-
details.push('Remapped IDs: ' + remappedIdStrings.join(', '))
225-
}
226-
if (result.skippedIds.length > 0) {
227-
const skippedRecordsForTables = {}
228-
result.skippedIds.forEach(id => {
229-
const table = id.split(':')[0]
230-
if (skippedRecordsForTables[table]) {
231-
skippedRecordsForTables[table] = skippedRecordsForTables[table] + 1
232-
} else {
233-
skippedRecordsForTables[table] = 1
234-
}
235-
})
236-
const skippedRecordsStrings = []
237-
for (let key in skippedRecordsForTables) {
238-
skippedRecordsStrings.push(`${key} - ${skippedRecordsForTables[key]}`)
239-
}
240-
details.push('Skipped records: ' + skippedRecordsStrings.join(', '))
241-
}
242-
if (result.idConflicts.length > 0) {
243-
// const conflicts = result.idConflicts
244-
details.push('ID conflicts: ' + result.idConflicts.join(', '))
245-
}
246-
dispatch(setStatusModal({
247-
title: result.failed
248-
? 'Warning: Errors encountered during feed merge!'
249-
: 'Feed merge was successful!',
250-
body: result.failed
251-
? `Merge failed with ${result.errorCount} errors. ${result.failureReasons.join(', ')}`
252-
: `Merge was completed successfully. A new version will be processed/validated containing the resulting feed.`,
253-
detail: details.join('\n')
254-
}))
260+
const modalContent = getMergeFeedModalContent(result)
261+
dispatch(setStatusModal(modalContent))
255262
}
256263
}
257264
break
258265
default:
259266
console.warn(`No completion step defined for job type ${job.type}`)
267+
break
260268
}
261269
}
262270
}

lib/manager/actions/versions.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export function fetchPublicFeedVersions (feedSource: Feed) {
190190
/**
191191
* Merges two feed versions according to the strategy defined within the
192192
*/
193-
export function mergeVersions (targetVersionId: string, versionId: string, mergeType: string) {
193+
export function mergeVersions (targetVersionId: string, versionId: string, mergeType: 'SERVICE_PERIOD' | 'REGIONAL') {
194194
return function (dispatch: dispatchFn, getState: getStateFn) {
195195
const url = `${SECURE_API_PREFIX}feedversion/merge?feedVersionIds=${targetVersionId},${versionId}&mergeType=${mergeType}`
196196
return dispatch(secureFetch(url, 'put'))
@@ -316,11 +316,11 @@ export function fetchGTFSEntities ({
316316
}
317317
}
318318
`
319-
// If fetching for the editor, cast id to int for csv_line field
320-
return dispatch(fetchGraphQL({
321-
query,
322-
variables: {namespace, [entityIdField]: editor ? +id : id}
323-
}))
319+
const variables = !id
320+
? {namespace}
321+
// If fetching a single ID for the editor, cast id to int for csv_line field
322+
: {namespace, [entityIdField]: editor ? +id : id}
323+
return dispatch(fetchGraphQL({query, variables}))
324324
.then(data => {
325325
dispatch(receiveGTFSEntities({namespace, id, component: type, data, editor, replaceNew}))
326326
if (editor) {

lib/manager/components/version/FeedVersionDetails.js

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import * as versionsActions from '../../actions/versions'
1818
import {getConfigProperty, isExtensionEnabled} from '../../../common/util/config'
1919
import {BLOCKING_ERROR_TYPES} from '../../util/version'
2020
import VersionDateLabel from './VersionDateLabel'
21+
import VersionRetrievalBadge from './VersionRetrievalBadge'
22+
import VersionSelectorDropdown from './VersionSelectorDropdown'
2123

2224
import type {FeedVersion, GtfsPlusValidation, Bounds, Feed} from '../../../types'
2325
import type {ManagerUserState} from '../../../types/reducers'
@@ -51,12 +53,30 @@ export default class FeedVersionDetails extends Component<Props> {
5153
!!(errorCounts.find(ec => BLOCKING_ERROR_TYPES.indexOf(ec.type) !== -1))
5254
}
5355

56+
_mergeItemFormatter = (v: FeedVersion, i: number, activeVersion: FeedVersion) => {
57+
const name = v.id === activeVersion.id
58+
? '(Cannot merge with self)'
59+
: v.retrievalMethod === 'SERVICE_PERIOD_MERGE'
60+
? '(Cannot re-merge feed)'
61+
: v.name
62+
const disabled = v.retrievalMethod === 'SERVICE_PERIOD_MERGE' ||
63+
v.id === activeVersion.id
64+
return (
65+
<MenuItem
66+
key={v.id}
67+
disabled={disabled}
68+
eventKey={disabled ? null : v.id}
69+
>
70+
{v.version}. {name}{' '}
71+
<VersionRetrievalBadge version={v} />
72+
</MenuItem>
73+
)
74+
}
75+
5476
_handleMergeVersion = (versionId: string) => {
55-
// For now, merging feeds only works for the MTC extension. It will fail
56-
// when the 'none' merge type is used.
57-
// TODO: add support for other merge types.
58-
const mergeType = isExtensionEnabled('mtc') ? 'MTC' : 'none'
59-
this.props.mergeVersions(this.props.version.id, versionId, mergeType)
77+
// Note: service period feed merge has only been extensively tested with
78+
// MTC-specific logic.
79+
this.props.mergeVersions(this.props.version.id, versionId, 'SERVICE_PERIOD')
6080
}
6181

6282
_onClickPublish = () => this.props.publishFeedVersion(this.props.version)
@@ -81,6 +101,10 @@ export default class FeedVersionDetails extends Component<Props> {
81101
)
82102
const hasBlockingIssue = this._checkBlockingIssue(version)
83103
const hasGtfsPlusBlockingIssue = gtfsPlusValidation && gtfsPlusValidation.issues.length > 0
104+
const isMergedServicePeriods = version.retrievalMethod === 'SERVICE_PERIOD_MERGE'
105+
const mergeButtonLabel = isMergedServicePeriods
106+
? 'Cannot re-merge feed'
107+
: 'Merge with version'
84108
return (
85109
<ListGroupItem>
86110
<h4>
@@ -89,21 +113,17 @@ export default class FeedVersionDetails extends Component<Props> {
89113
{// Only show merge feeds button if the feed starts in the future.
90114
// FIXME: uncomment out the below to prevent merges with non-future feeds.
91115
// moment(summary.startDate).isAfter(moment().startOf('day')) &&
92-
<DropdownButton
93-
id={'merge-versions-dropdown'}
94-
title={<span><Icon type='code-fork' /> Merge with version</span>}
95-
onSelect={this._handleMergeVersion}>
96-
{feedSource.feedVersions && feedSource.feedVersions.map((v, i) => {
97-
if (v.id === version.id) {
98-
return (
99-
<MenuItem key={v.id} disabled eventKey={null}>
100-
{v.version}. (Cannot merge with self)
101-
</MenuItem>
102-
)
103-
}
104-
return <MenuItem key={v.id} eventKey={v.id}>{v.version}. {v.name}</MenuItem>
105-
})}
106-
</DropdownButton>
116+
<VersionSelectorDropdown
117+
dropdownProps={{
118+
id: 'merge-versions-dropdown',
119+
disabled: isMergedServicePeriods,
120+
onSelect: this._handleMergeVersion
121+
}}
122+
title={<span><Icon type='code-fork' /> {mergeButtonLabel}</span>}
123+
itemFormatter={this._mergeItemFormatter}
124+
version={version}
125+
versions={feedSource.feedVersions}
126+
/>
107127
}
108128
<Button
109129
disabled={

lib/manager/components/version/FeedVersionNavigator.js

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import * as gtfsPlusActions from '../../../gtfsplus/actions/gtfsplus'
1818
import * as deploymentActions from '../../../manager/actions/deployments'
1919
import DeploymentPreviewButton from '../DeploymentPreviewButton'
2020
import FeedVersionViewer from './FeedVersionViewer'
21+
import VersionSelectorDropdown from './VersionSelectorDropdown'
2122

2223
import type {Props as ContainerProps} from '../../containers/ActiveFeedVersionNavigator'
2324
import type {FeedVersion, GtfsPlusValidation, Note} from '../../../types'
@@ -146,17 +147,17 @@ export default class FeedVersionNavigator extends Component<Props, State> {
146147
renameFeedVersion,
147148
sortedVersions,
148149
user,
150+
version,
149151
versionSection
150152
} = this.props
151-
const {feedVersions: versions} = feedSource
152-
if (!versions) return null
153+
if (sortedVersions.length === 0) return null
153154

154155
if (typeof feedVersionIndex === 'undefined') return null
155-
const version = hasVersions && versions[feedVersionIndex - 1]
156156
const deploymentForVersion =
157157
version &&
158158
feedSource.deployments &&
159159
feedSource.deployments.find(d => d.feedVersions.findIndex(v => v.id === version.id) !== -1)
160+
const dropdownTitle = `${this.messages('version')} ${feedVersionIndex} ${this.messages('of')} ${sortedVersions.length}`
160161
return (
161162
<div>
162163
<SelectFileModal ref='uploadModal'
@@ -195,16 +196,16 @@ export default class FeedVersionNavigator extends Component<Props, State> {
195196
</Button>
196197

197198
{/* Version Selector Dropdown */}
198-
<DropdownButton
199-
href='#'
200-
id='versionSelector'
201-
title={`${this.messages('version')} ${feedVersionIndex} ${this.messages('of')} ${versions.length}`}
202-
onSelect={this._onSelectVersion}>
203-
{versions.map((version, k) => {
204-
k = k + 1
205-
return <MenuItem key={k} eventKey={k}>{k}. {version.name}</MenuItem>
206-
})}
207-
</DropdownButton>
199+
<VersionSelectorDropdown
200+
dropdownProps={{
201+
id: 'versionSelector',
202+
onSelect: this._onSelectVersion
203+
}}
204+
header='Select a version to view summary'
205+
title={dropdownTitle}
206+
version={version}
207+
versions={sortedVersions}
208+
/>
208209
{/* Next Version Button */}
209210
<Button href='#'
210211
disabled={!hasVersions || !sortedVersions[feedVersionIndex]}
@@ -312,7 +313,7 @@ export default class FeedVersionNavigator extends Component<Props, State> {
312313
user={user}
313314
version={this.props.version}
314315
versionSection={versionSection || null}
315-
versions={versions} />
316+
versions={sortedVersions} />
316317
</Col>
317318
</Row>
318319
</div>

lib/manager/components/version/FeedVersionReport.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import FeedVersionDetails from './FeedVersionDetails'
1212
import FeedVersionMap from './FeedVersionMap'
1313
import FeedVersionTabs from './FeedVersionTabs'
1414
import VersionButtonToolbar from './VersionButtonToolbar'
15+
import VersionRetrievalBadge from './VersionRetrievalBadge'
1516

1617
import type {Props as FeedVersionViewerProps} from './FeedVersionViewer'
1718

@@ -113,7 +114,7 @@ export default class FeedVersionReport extends Component<Props, State> {
113114
<VersionButtonToolbar size='small' {...this.props} />
114115
</h4>
115116
<small title={moment(updated).format(dateFormat + ', ' + timeFormat)}>
116-
<Icon type='clock-o' />
117+
<VersionRetrievalBadge version={version} />
117118
{' '}
118119
Version published {moment(updated).fromNow()} by {userLink}
119120
</small>

0 commit comments

Comments
 (0)