Skip to content

Commit 70adc13

Browse files
Merge pull request #1030 from ibi-group/override-feed-filenames
feat: support overriding feed filenames
2 parents 2408542 + bd6b320 commit 70adc13

File tree

9 files changed

+119
-9
lines changed

9 files changed

+119
-9
lines changed

i18n/english.yml

+6
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ components:
319319
FeedInfo:
320320
autoFetch: Auto fetch
321321
autoPublish: Auto publish
322+
bundleFilename: Bundle Filename
322323
dateFormat: MMM D, YYYY
323324
deployable: Deployable
324325
edit: Edit
@@ -661,6 +662,8 @@ components:
661662
title: Updates
662663
labels:
663664
title: Labels
665+
bundleFilename: Override filename for GTFS bundle
666+
bundleFilenameHint: Optional. Define a custom filename (without .zip) for this feed source when included in a GTFS bundle. The .zip extension is added automatically.
664667
make:
665668
private: Make private
666669
privateDesc: Make this feed source private.
@@ -673,6 +676,8 @@ components:
673676
rename: Rename
674677
save: Save
675678
title: Settings
679+
invalidFilename: "Filename contains invalid characters (e.g., / \ : * ? \" < > | space) or ends with .zip."
680+
filenamePlaceholder: e.g., agency_bundle
676681
GtfsIcons:
677682
agency:
678683
title: Edit agencies
@@ -759,6 +764,7 @@ components:
759764
title: Log in
760765
ManagerHeader:
761766
noUpdateYet: n/a
767+
bundleFilename: Bundle Filename
762768
noUrl: (none)
763769
private: This feed source and all its versions are private.
764770
ManagerPage:

i18n/german.yml

+6
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ components:
342342
FeedInfo:
343343
autoFetch: Automatischer Abruf
344344
autoPublish: Automatische Veröffentlichung
345+
bundleFilename: Bundle Filename
345346
dateFormat: DD.MM.YYYY
346347
deployable: Deploybar
347348
edit: Bearbeiten
@@ -676,6 +677,10 @@ components:
676677
title: Aktualisierungen
677678
labels:
678679
title: Ettiketten
680+
bundleFilename: Override filename for GTFS bundle
681+
bundleFilenameHint: Optional. Define a custom filename (without .zip) for this feed source when included in a GTFS bundle. The .zip extension is added automatically.
682+
invalidFilename: "Filename contains invalid characters (e.g., / \ : * ? \" < > | space) or ends with .zip."
683+
filenamePlaceholder: e.g., agency_bundle
679684
make:
680685
private: Privat setzen
681686
privateDesc: Setzt diese Feed-Quelle auf Privat.
@@ -769,6 +774,7 @@ components:
769774
title: Anmelden
770775
ManagerHeader:
771776
noUpdateYet: keine
777+
bundleFilename: Bundle Filename
772778
noUrl: (keine URL)
773779
private: Diese Feed-Quelle und all ihre Versionen sind privat.
774780
ManagerPage:

i18n/polish.yml

+6
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ components:
339339
FeedInfo:
340340
autoFetch: Automatyczne pobieranie
341341
autoPublish: Automatycznie publikuj
342+
bundleFilename: Bundle Filename
342343
dateFormat: MMM D, YYYY
343344
deployable: Rozmieszczany
344345
edit: Edytować
@@ -599,6 +600,8 @@ components:
599600
title: Automatic Fetch
600601
url: Feed source fetch URL
601602
urlButton: Change URL
603+
bundleFilename: Override filename for GTFS bundle
604+
bundleFilenameHint: Optional. Define a custom filename (without .zip) for this feed source when included in a GTFS bundle. The .zip extension is added automatically.
602605
confirmDelete: Are you sure you want to delete this project? This action cannot
603606
be undone and all feed sources and their versions will be permanently deleted.
604607
dangerZone: Danger Zone
@@ -668,6 +671,8 @@ components:
668671
title: Updates
669672
labels:
670673
title: Labels
674+
invalidFilename: "Filename contains invalid characters (e.g., / \ : * ? \" < > | space) or ends with .zip."
675+
filenamePlaceholder: e.g., agency_bundle
671676
make:
672677
private: Make private
673678
privateDesc: Make this feed source private.
@@ -760,6 +765,7 @@ components:
760765
title: Log in
761766
ManagerHeader:
762767
noUpdateYet: n/a
768+
bundleFilename: Bundle Filename
763769
noUrl: (none)
764770
private: This feed source and all its versions are private.
765771
ManagerPage:

lib/manager/components/CreateFeedSource.js

+19-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import Loading from '../../common/components/Loading'
2323
import {FREQUENCY_INTERVALS} from '../../common/constants'
2424
import {isExtensionEnabled} from '../../common/util/config'
2525
import {validationState} from '../util'
26+
import {isValidFilename} from '../util/validation'
2627
import type {FetchFrequency, NewFeed} from '../../types'
2728

2829
import FeedFetchFrequency from './FeedFetchFrequency'
@@ -35,6 +36,7 @@ type Props = {
3536

3637
type Validation = {
3738
_form: boolean,
39+
filename?: boolean,
3840
name?: boolean,
3941
url?: boolean
4042
}
@@ -77,12 +79,14 @@ export default class CreateFeedSource extends Component<Props, State> {
7779
deployable: false,
7880
fetchFrequency: 'DAYS',
7981
fetchInterval: 1,
82+
filename: '',
8083
name: '',
8184
projectId: props.projectId,
8285
url: ''
8386
},
8487
validation: {
8588
_form: false,
89+
filename: true,
8690
name: true,
8791
url: true
8892
}
@@ -145,10 +149,11 @@ export default class CreateFeedSource extends Component<Props, State> {
145149
_validateModel (model: NewFeed) {
146150
const validation: Validation = {
147151
_form: false,
148-
name: !(!model.name || model.name.length === 0),
152+
filename: isValidFilename(model.filename),
153+
name: !!model.name && model.name.length > 0,
149154
url: !model.url || validator.isURL(model.url)
150155
}
151-
validation._form = !!(validation.name && validation.url)
156+
validation._form = !!(validation.name && validation.url && validation.filename)
152157
this.setState({ validation })
153158
}
154159

@@ -194,6 +199,18 @@ export default class CreateFeedSource extends Component<Props, State> {
194199
individually.
195200
</small>
196201
</FormGroup>
202+
<FormGroup validationState={validationState(validation.filename)}>
203+
<ControlLabel>Override filename for bundle (optional)</ControlLabel>
204+
<FormControl
205+
disabled={!model.deployable}
206+
name={'filename'}
207+
onChange={this._onInputChange('filename')}
208+
placeholder='e.g., agency_bundle'
209+
value={model.filename}
210+
/>
211+
<FormControl.Feedback />
212+
{!validation.filename && <HelpBlock>Invalid filename</HelpBlock>}
213+
</FormGroup>
197214
</ListGroupItem>
198215
</ListGroup>
199216
</Panel>

lib/manager/components/FeedSourceTableRow.js

+9
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,15 @@ class FeedInfo extends PureComponent<{ feedSource: Feed, project: Project, user:
299299
{this.messages('autoPublish')}
300300
</Col>
301301
)}
302+
{feedSource.filename && (
303+
<Col xs={12}>
304+
<Row>
305+
<Col xs={12}>
306+
<Icon type='file-text-o' />{this.messages('bundleFilename')}: {feedSource.filename}.zip
307+
</Col>
308+
</Row>
309+
</Col>
310+
)}
302311
<Col xs={12}>
303312
<div className='feedSourceLabelRow'>
304313
{project.labels.length > 0 && (

lib/manager/components/GeneralSettings.js

+52-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ControlLabel,
1010
FormControl,
1111
FormGroup,
12+
HelpBlock,
1213
InputGroup,
1314
ListGroup,
1415
ListGroupItem,
@@ -18,6 +19,7 @@ import {
1819
import * as feedsActions from '../actions/feeds'
1920
import { FREQUENCY_INTERVALS } from '../../common/constants'
2021
import {getComponentMessages} from '../../common/util/config'
22+
import {isValidFilename} from '../util/validation'
2123
import LabelAssigner from '../components/LabelAssigner'
2224
import type { Feed, FetchFrequency, Project } from '../../types'
2325
import type { ManagerUserState } from '../../types/reducers'
@@ -34,27 +36,44 @@ type Props = {
3436
}
3537

3638
type State = {
39+
filename?: ?string,
40+
filenameError?: ?string,
41+
filenameValidationState?: ?string,
3742
name?: ?string,
3843
url?: ?string
3944
}
4045

4146
export default class GeneralSettings extends Component<Props, State> {
4247
messages = getComponentMessages('GeneralSettings')
43-
state = {}
48+
state: State = {
49+
filename: undefined,
50+
filenameError: null,
51+
filenameValidationState: null,
52+
name: undefined,
53+
url: undefined
54+
}
4455

4556
_onChange = ({target}: SyntheticInputEvent<HTMLInputElement>) => {
4657
// Change empty string to null to avoid setting URL to empty string value.
4758
let value = target.value || null
4859
if (target.name === 'url' && value) value = value.trim()
49-
this.setState({[target.name]: value})
60+
const newState: State = {[target.name]: value}
61+
// Validate filename if changed
62+
if (target.name === 'filename') {
63+
// Check for invalid characters and ensure it doesn't end with .zip (case-insensitive)
64+
const isValid = isValidFilename(value)
65+
newState.filenameValidationState = isValid ? 'success' : 'error'
66+
newState.filenameError = isValid ? null : this.messages('invalidFilename')
67+
}
68+
this.setState(newState)
5069
}
5170

5271
_onToggleDeployable = () => {
5372
const {feedSource, updateFeedSource} = this.props
5473
updateFeedSource(feedSource, {deployable: !feedSource.deployable})
5574
}
5675

57-
_getFormValue = (key: 'name' | 'url') => {
76+
_getFormValue = (key: 'name' | 'url' | 'filename') => {
5877
// If state value does not exist (i.e., form is unedited), revert to value
5978
// from props.
6079
const value = typeof this.state[key] === 'undefined'
@@ -108,6 +127,11 @@ export default class GeneralSettings extends Component<Props, State> {
108127
updateFeedSource(feedSource, {url: this.state.url})
109128
}
110129

130+
_onSaveFilename = () => {
131+
const {feedSource, updateFeedSource} = this.props
132+
updateFeedSource(feedSource, {filename: this.state.filename})
133+
}
134+
111135
render () {
112136
const {
113137
confirmDeleteFeedSource,
@@ -117,7 +141,8 @@ export default class GeneralSettings extends Component<Props, State> {
117141
} = this.props
118142
const {
119143
name,
120-
url
144+
url,
145+
filename
121146
} = this.state
122147
const autoFetchFeed = feedSource.retrievalMethod === 'FETCHED_AUTOMATICALLY'
123148
return (
@@ -157,6 +182,29 @@ export default class GeneralSettings extends Component<Props, State> {
157182
<small>{this.messages('makeDeployableHint')}</small>
158183
</FormGroup>
159184
</ListGroupItem>
185+
<ListGroupItem>
186+
<FormGroup validationState={this.state.filenameValidationState}>
187+
<ControlLabel>{this.messages('bundleFilename')}</ControlLabel>
188+
<InputGroup>
189+
<FormControl
190+
disabled={disabled || !feedSource.deployable}
191+
placeholder={this.messages('filenamePlaceholder')}
192+
name={'filename'}
193+
onChange={this._onChange}
194+
value={this._getFormValue('filename')} />
195+
<InputGroup.Button>
196+
<Button
197+
// disable if no change or value is unchanged
198+
disabled={disabled || typeof filename === 'undefined' || filename === feedSource.filename || this.state.filenameValidationState === 'error'}
199+
onClick={this._onSaveFilename}>
200+
{this.messages('save')}
201+
</Button>
202+
</InputGroup.Button>
203+
</InputGroup>
204+
<small>{this.messages('bundleFilenameHint')}</small>
205+
{this.state.filenameError && <HelpBlock>{this.state.filenameError}</HelpBlock>}
206+
</FormGroup>
207+
</ListGroupItem>
160208
</ListGroup>
161209
</Panel>
162210
<Panel>

lib/manager/components/ManagerHeader.js

+5
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ export default class ManagerHeader extends Component<Props> {
100100
<Icon type='file-archive-o' />{' '}
101101
{this.getAverageFileSize(feedSource.feedVersionSummaries)}
102102
</li>
103+
{feedSource.filename && (
104+
<li>
105+
<Icon type='file-text-o' /> {this.messages('bundleFilename')}: {feedSource.filename}.zip
106+
</li>
107+
)}
103108
</ul>
104109
</Col>
105110
</Row>

lib/manager/util/validation.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// @flow
2+
3+
/**
4+
* Checks if a filename is valid.
5+
* A valid filename does not contain <>:"/\|?* characters and does not end with .zip (case-insensitive).
6+
* An empty or null/undefined filename is also considered valid.
7+
*/
8+
export function isValidFilename (filename: ?string): boolean {
9+
// Ensure only valid characters (no ., <>,:"/\\|?*, or space) are used
10+
return !filename || /^[^<>:"/\\|?* .]*$/.test(filename)
11+
}

lib/types/index.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -416,10 +416,11 @@ export type Feed = FeedSourceSummary & {
416416
feedVersions?: Array<FeedVersion>,
417417
fetchFrequency?: FetchFrequency,
418418
fetchInterval?: number,
419+
filename?: ?string,
419420
isCreating?: boolean,
420421
lastFetched?: ?number,
421-
latestVersionId?: ?string,
422-
name: string, // Flow throws an error if we don't duplicate this property in the types here.
422+
latestVersionId?: ?string, // Flow throws an error if we don't duplicate this property in the types here.
423+
name: string,
423424
noteCount: number,
424425
notes?: Array<Note>,
425426
organizationId: ?string,
@@ -440,10 +441,11 @@ export type NewFeed = {
440441
deployable?: boolean,
441442
fetchFrequency: FetchFrequency,
442443
fetchInterval: number,
444+
filename?: string,
443445
name?: string,
444446
projectId: string,
445447
retrievalMethod?: string,
446-
url?: string
448+
url?: string,
447449
}
448450

449451
type ValidationErrorPriority = 'HIGH' | 'MEDIUM' | 'LOW' | 'UNKNOWN'

0 commit comments

Comments
 (0)