diff --git a/assets/images/ui/repo-settings.png b/assets/images/ui/repo-settings.png
new file mode 100644
index 00000000..96fe3fa5
Binary files /dev/null and b/assets/images/ui/repo-settings.png differ
diff --git a/assets/scripts/app/auth.coffee b/assets/scripts/app/auth.coffee
index 40a47c79..46dda80f 100644
--- a/assets/scripts/app/auth.coffee
+++ b/assets/scripts/app/auth.coffee
@@ -71,7 +71,7 @@
try
router.send('afterSignIn')
catch e
- throw e unless e =~ /There are no active handlers/
+ throw e unless e =~ /There are no active handlers/ || e =~ /Can't trigger action "afterSignIn/
@refreshUserData(data.user)
refreshUserData: (user) ->
diff --git a/assets/scripts/app/components.coffee b/assets/scripts/app/components.coffee
index da423edd..09a177fb 100644
--- a/assets/scripts/app/components.coffee
+++ b/assets/scripts/app/components.coffee
@@ -1,9 +1,19 @@
Travis.TravisSwitchComponent = Ember.Component.extend
tagName: 'a'
classNames: ['travis-switch']
- classNameBindings: ['active']
+ classNameBindings: ['_active:active']
- activeBinding: 'target.active'
+ # TODO: how to handle overriding properties to
+ # avoid naming it _action?
+ _active: (->
+ @get('target.active') || @get('active')
+ ).property('target.active', 'active')
click: ->
- @sendAction('action', @get('target'))
+ if target = @get('target')
+ @set('target.active', !@get('target.active'))
+ else
+ @set('active', !@get('active'))
+ # allow for bindings to propagate
+ Ember.run.next this, ->
+ @sendAction('action', target)
diff --git a/assets/scripts/app/controllers.coffee b/assets/scripts/app/controllers.coffee
index c073461a..3257b5d4 100644
--- a/assets/scripts/app/controllers.coffee
+++ b/assets/scripts/app/controllers.coffee
@@ -50,6 +50,22 @@ Travis.FirstSyncController = Em.Controller.extend
Travis.IndexErrorController = Em.Controller.extend()
+Travis.RepoSettingsTabController = Em.ObjectController.extend()
+Travis.RepoSettingsController = Em.ObjectController.extend
+ needs: ['repoSettingsTab']
+ tab: Ember.computed.alias('controllers.repoSettingsTab.model.tab')
+ settings: Ember.computed.alias('model.settings')
+
+ save: ->
+ @set('saving', true)
+ self = this
+ @get('model').saveSettings(@get('settings')).then ->
+ self.set('saving', false)
+ Travis.flash(success: 'Settings were saved successfully')
+ , ->
+ self.set('saving', false)
+ Travis.flash(error: 'There was an error while saving settings. Please try again.')
+
require 'controllers/accounts'
require 'controllers/build'
require 'controllers/builds'
diff --git a/assets/scripts/app/helpers/handlebars.coffee b/assets/scripts/app/helpers/handlebars.coffee
index 8dfda033..1074da90 100644
--- a/assets/scripts/app/helpers/handlebars.coffee
+++ b/assets/scripts/app/helpers/handlebars.coffee
@@ -3,6 +3,293 @@ require 'ext/ember/bound_helper'
safe = (string) ->
new Handlebars.SafeString(string)
+Travis.Tab = Ember.Object.extend
+ show: ->
+ @get('tabs').forEach( (t) -> t.hide() )
+ @set('visible', true)
+
+ hide: ->
+ @set('visible', false)
+
+Travis.TabsView = Ember.View.extend
+ tabBinding: 'controller.tab'
+ tabsBinding: 'controller.tabs'
+
+ tabDidChange: (->
+ @activateTab(@get('tab'))
+ ).observes('tab')
+
+ tabsDidChange: (->
+ tab = @get('tab')
+ if tab
+ @activateTab(tab)
+ else if @get('tabs.length')
+ @activateTab(@get('tabs.firstObject.id'))
+ ).observes('tabs.length', 'tabs')
+
+ activateTab: (tabId) ->
+ tab = @get('tabs').findBy('id', tabId)
+
+ return unless tab
+
+ tab.show() unless tab.get('visible')
+
+ # TODO: remove hardcoded link
+ layout: Ember.Handlebars.compile(
+ '
' +
+ ' {{#each tab in tabs}}' +
+ ' - ' +
+ '
{{#linkTo "repo.settings.tab" tab.id}}{{tab.name}}{{/linkTo}}
' +
+ ' ' +
+ ' {{/each}}' +
+ '
' +
+ '{{yield}}')
+
+Travis.TabView = Ember.View.extend
+ attributeBindings: ['style']
+
+ style: (->
+ if !@get('tab.visible')
+ 'display: none'
+ ).property('tab.visible')
+
+Ember.Handlebars.registerHelper('travis-tab', (id, name, options) ->
+ controller = this
+ controller.set('tabs', []) unless controller.get('tabs')
+
+ tab = Travis.Tab.create(id: id, name: name, tabs: controller.get('tabs'))
+
+ view = Travis.TabView.create(
+ controller: this
+ tab: tab
+ )
+
+ controller = this
+ Ember.run.schedule('afterRender', ->
+ if controller.get('tabs.length') == 0
+ tab.show()
+ controller.get('tabs').pushObject(tab)
+ )
+
+ Ember.Handlebars.helpers.view.call(this, view, options)
+)
+
+
+Ember.Handlebars.registerHelper('travis-tabs', (options) ->
+ template = options.fn
+ delete options.fn
+
+ @set('tabs', [])
+
+ view = Travis.TabsView.create(
+ controller: this
+ template: template
+ )
+
+ Ember.Handlebars.helpers.view.call(this, view, options)
+)
+
+Travis.SettingsMultiplierView = Ember.CollectionView.extend()
+
+createObjects = (path, offset) ->
+ segments = path.split('.')
+ if segments.length > offset
+ for i in [1..(segments.length - offset)]
+ path = segments.slice(0, i).join('.')
+ if Ember.isNone(Ember.get(this, path))
+ Ember.set(this, path, {})
+
+ return segments
+
+Ember.Handlebars.registerHelper('settings-multiplier', (path, options) ->
+ template = options.fn
+ delete options.fn
+
+ parentsPath = getSettingsPath(options.data.view)
+ if parentsPath && parentsPath != ''
+ path = parentsPath + '.' + path
+
+ createObjects.call(this, path, 1)
+
+ if Ember.isNone(@get(path))
+ collection = [{}]
+ @set(path, collection)
+
+
+ itemViewClass = Ember.View.extend(
+ template: template,
+ controller: this,
+ tagName: 'li',
+ multiplier: true
+ )
+
+ view = Travis.SettingsMultiplierView.create(
+ contentBinding: 'controller.' + path
+ controller: this
+ tagName: 'ul'
+ itemViewClass: itemViewClass
+ fields: []
+ settingsPath: path
+ )
+
+ view.addObserver('content.length', ->
+ if @get('content.length') == 0
+ @get('content').pushObject({})
+ )
+
+ Ember.Handlebars.helpers.view.call(this, view, options)
+)
+
+Travis.FormSettingsView = Ember.View.extend Ember.TargetActionSupport,
+ target: Ember.computed.alias('controller')
+ actionContext: Ember.computed.alias('context'),
+ action: 'submit'
+ tagName: 'form'
+ submit: (event) ->
+ event.preventDefault()
+ @triggerAction()
+
+
+Ember.Handlebars.registerHelper('settings-form', (path, options) ->
+ if arguments.length == 1
+ options = path
+ path = 'settings'
+
+ view = Travis.FormSettingsView.create(
+ template: options.fn
+ controller: this
+ settingsPath: path
+ )
+
+ delete options.fn
+
+ Ember.Handlebars.helpers.view.call(this, view, options)
+)
+
+Ember.Handlebars.helper('settings-select', (options) ->
+ view = options.data.view
+ optionValues = options.hash.options
+ delete options.hash.options
+
+ originalPath = options.hash.value
+
+ parentsPath = getSettingsPath(view)
+ #TODO: such checks should also check parents, not only current context view
+ if !view.get('multiplier') && parentsPath && parentsPath != ''
+ originalPath = parentsPath + '.' + originalPath
+
+ fullPath = originalPath
+
+ if view.get('multiplier')
+ fullPath = 'view.content.' + fullPath
+
+ createObjects.call(this, fullPath, 1)
+
+ # TODO: setting a value here does not work and we still need
+ # a valueBinding in the view, I'm not sure why
+ options.hash.value = fullPath
+
+ selectView = Ember.Select.create(
+ content: [''].pushObjects(optionValues.split(','))
+ controller: this
+ valueBinding: 'controller.' + fullPath
+ )
+
+ Ember.Handlebars.helpers.view.call(this, selectView, options)
+)
+
+
+
+Ember.Handlebars.helper('settings-remove-link', (options) ->
+ view = Ember.View.extend(
+ tagName: 'a'
+ attributeBindings: ['href', 'style']
+ href: '#'
+ style: (->
+ # TODO: do not assume that we're direct child
+ if @get('parentView.parentView.content.length') == 1
+ 'display: none'
+ ).property('parentView.parentView.content.length')
+ template: Ember.Handlebars.compile('remove')
+ controller: this
+ click: (event) ->
+ event.preventDefault()
+
+ if content = @get('parentView.content')
+ @get('parentView.parentView.content').removeObject(content)
+ ).create()
+
+ Ember.Handlebars.helpers.view.call(this, view, options)
+)
+
+Ember.Handlebars.registerHelper('settings-add-link', (path, options) ->
+ parentsPath = getSettingsPath(options.data.view)
+ if parentsPath && parentsPath != ''
+ path = parentsPath + '.' + path
+
+ view = Ember.View.create(
+ tagName: 'a'
+ attributeBindings: ['href']
+ href: '#'
+ template: options.fn
+ controller: this
+ click: (event) ->
+ event.preventDefault()
+
+ if collection = @get('controller.' + path)
+ collection.pushObject({})
+ )
+
+ Ember.Handlebars.helpers.view.call(this, view, options)
+)
+
+getSettingsPath = (view) ->
+ settingsPaths = []
+ if settingsPath = view.get('settingsPath')
+ settingsPaths.pushObject settingsPath
+
+ parent = view
+ while parent = parent.get('parentView')
+ if settingsPath = parent.get('settingsPath')
+ settingsPaths.pushObject settingsPath
+
+ return settingsPaths.reverse().join('.')
+
+Ember.Handlebars.helper('settings-input', (options) ->
+ view = options.data.view
+
+ if options.hash.type == 'checkbox'
+ originalPath = options.hash.checked
+ else
+ originalPath = options.hash.value
+
+ parentsPath = getSettingsPath(view)
+ #TODO: such checks should also check parents, not only current context view
+ if !view.get('multiplier') && parentsPath && parentsPath != ''
+ originalPath = parentsPath + '.' + originalPath
+
+ fullPath = originalPath
+
+ if view.get('multiplier')
+ fullPath = 'view.content.' + fullPath
+
+ if options.hash.type != 'password'
+ createObjects.call(this, fullPath, 1)
+ else
+ createObjects.call(view, fullPath, 2)
+ content = view.get('content')
+ fullPath += ".value"
+ if Ember.isNone(Ember.get(content, originalPath))
+ Ember.set(content, originalPath, {})
+ Ember.set(content, originalPath + ".type", 'password')
+
+ if options.hash.type == 'checkbox'
+ options.hash.checked = fullPath
+ else
+ options.hash.value = fullPath
+ Ember.Handlebars.helpers.input.call(this, options)
+)
+
Handlebars.registerHelper 'tipsy', (text, tip) ->
safe '' + text + ''
diff --git a/assets/scripts/app/models/hook.coffee b/assets/scripts/app/models/hook.coffee
index 677708d1..4126292b 100644
--- a/assets/scripts/app/models/hook.coffee
+++ b/assets/scripts/app/models/hook.coffee
@@ -27,3 +27,13 @@ require 'travis/model'
return if @get('isSaving')
@set 'active', !@get('active')
@save()
+
+ repo: (->
+ # I don't want to make an ajax request for each repository showed in profile,
+ # especially, because most of them does not have any builds anyway. That's why
+ # I add an info which we have here to the store - this will allow to display
+ # a link to the repo and if more info is needed, it will be requested when the
+ # link is used
+ Travis.loadOrMerge(Travis.Repo, @getProperties('id', 'slug', 'name', 'ownerName'), skipIfExists: true)
+ Travis.Repo.find(@get('id'))
+ ).property('id')
diff --git a/assets/scripts/app/models/repo.coffee b/assets/scripts/app/models/repo.coffee
index e1b835de..3f94fc93 100644
--- a/assets/scripts/app/models/repo.coffee
+++ b/assets/scripts/app/models/repo.coffee
@@ -105,6 +105,13 @@ require 'travis/model'
regenerateKey: (options) ->
Travis.ajax.ajax '/repos/' + @get('id') + '/key', 'post', options
+ fetchSettings: ->
+ Travis.ajax.ajax('/repos/' + @get('id') + '/settings', 'get', forceAuth: true).then (data) ->
+ data['settings']
+
+ saveSettings: (settings) ->
+ Travis.ajax.ajax('/repos/' + @get('id') + '/settings', 'patch', data: { settings: settings })
+
@Travis.Repo.reopenClass
recent: ->
@find()
diff --git a/assets/scripts/app/routes.coffee b/assets/scripts/app/routes.coffee
index 523e5f96..f277b20d 100644
--- a/assets/scripts/app/routes.coffee
+++ b/assets/scripts/app/routes.coffee
@@ -71,6 +71,12 @@ Travis.ApplicationRoute = Travis.Route.extend
afterSignOut: ->
@afterSignOut()
+Travis.Router.reopen
+ transitionTo: ->
+ this.container.lookup('controller:repo').set('lineNumber', null)
+
+ @_super.apply this, arguments
+
Travis.Router.map ->
@resource 'index', path: '/', ->
@route 'current', path: '/'
@@ -82,6 +88,11 @@ Travis.Router.map ->
@resource 'pullRequests', path: '/pull_requests'
@resource 'branches', path: '/branches'
+ # this can't be nested in repo, because we want a set of different
+ # templates rendered for settings (for example no "current", "builds", ... tabs)
+ @resource 'repo.settings', path: '/:owner/:name/settings', ->
+ @route 'tab', path: ':tab'
+
@route 'getting_started'
@route 'first_sync'
@route 'stats', path: '/stats'
@@ -105,7 +116,7 @@ Travis.SetupLastBuild = Ember.Mixin.create
repo = @controllerFor('repo').get('repo')
if repo && repo.get('isLoaded') && !repo.get('lastBuildId')
Ember.run.next =>
- @render('builds/not_found', outlet: 'pane', into: 'repo')
+ @render('builds/not_found', into: 'repo', outlet: 'pane')
Travis.GettingStartedRoute = Travis.Route.extend
setupController: ->
@@ -138,7 +149,7 @@ Travis.FirstSyncRoute = Travis.Route.extend
Travis.IndexCurrentRoute = Travis.Route.extend Travis.SetupLastBuild,
renderTemplate: ->
@render 'repo'
- @render 'build', outlet: 'pane', into: 'repo'
+ @render 'build', into: 'repo', outlet: 'pane'
setupController: ->
@_super.apply this, arguments
@@ -155,7 +166,7 @@ Travis.IndexCurrentRoute = Travis.Route.extend Travis.SetupLastBuild,
Travis.AbstractBuildsRoute = Travis.Route.extend
renderTemplate: ->
- @render 'builds', outlet: 'pane', into: 'repo'
+ @render 'builds', into: 'repo', outlet: 'pane'
setupController: ->
@controllerFor('repo').activate(@get('contentType'))
@@ -180,7 +191,7 @@ Travis.BranchesRoute = Travis.AbstractBuildsRoute.extend(contentType: 'branches'
Travis.BuildRoute = Travis.Route.extend
renderTemplate: ->
- @render 'build', outlet: 'pane', into: 'repo'
+ @render 'build', into: 'repo', outlet: 'pane'
serialize: (model, params) ->
id = if model.get then model.get('id') else model
@@ -201,7 +212,7 @@ Travis.BuildRoute = Travis.Route.extend
Travis.JobRoute = Travis.Route.extend
renderTemplate: ->
- @render 'job', outlet: 'pane', into: 'repo'
+ @render 'job', into: 'repo', outlet: 'pane'
serialize: (model, params) ->
id = if model.get then model.get('id') else model
@@ -228,7 +239,7 @@ Travis.RepoIndexRoute = Travis.Route.extend Travis.SetupLastBuild,
@controllerFor('repo').activate('current')
renderTemplate: ->
- @render 'build', outlet: 'pane', into: 'repo'
+ @render 'build', into: 'repo', outlet: 'pane'
Travis.RepoRoute = Travis.Route.extend
renderTemplate: ->
@@ -247,7 +258,6 @@ Travis.RepoRoute = Travis.Route.extend
model: (params) ->
slug = "#{params.owner}/#{params.name}"
-
Travis.Repo.fetchBySlug(slug)
actions:
@@ -306,7 +316,7 @@ Travis.ProfileRoute = Travis.Route.extend
@render 'top', outlet: 'top'
@render 'accounts', outlet: 'left'
@render 'flash', outlet: 'flash'
- @render 'profile'
+ @_super.apply(this, arguments)
Travis.ProfileIndexRoute = Travis.Route.extend
setupController: ->
@@ -374,3 +384,25 @@ Travis.AuthRoute = Travis.Route.extend
deactivate: ->
@controllerFor('auth').set('redirected', false)
+
+Travis.RepoSettingsRoute = Travis.Route.extend
+ setupController: (controller, model) ->
+ # TODO: if repo is just a data hash with id and slug load it
+ # as incomplete record
+ model = Travis.Repo.find(model.id) if model && !model.get
+ @_super(controller, model)
+
+ serialize: (repo) ->
+ slug = if repo.get then repo.get('slug') else repo.slug
+ [owner, name] = slug.split('/')
+ { owner: owner, name: name }
+
+ model: (params) ->
+ slug = "#{params.owner}/#{params.name}"
+ Travis.Repo.fetchBySlug(slug)
+
+ afterModel: (repo) ->
+ # I'm using afterModel to fetch settings, because model is not always called.
+ # If link-to already provides a model, it will be just set as a route context.
+ repo.fetchSettings().then (settings) ->
+ repo.set('settings', settings)
diff --git a/assets/scripts/app/templates/components/travis-switch.hbs b/assets/scripts/app/templates/components/travis-switch.hbs
index e7b20837..18617602 100644
--- a/assets/scripts/app/templates/components/travis-switch.hbs
+++ b/assets/scripts/app/templates/components/travis-switch.hbs
@@ -1,4 +1,4 @@
-{{#if active}}
+{{#if _active}}
ON
{{else}}
OFF
diff --git a/assets/scripts/app/templates/profile/repo.hbs b/assets/scripts/app/templates/profile/repo.hbs
new file mode 100644
index 00000000..43d6cf6c
--- /dev/null
+++ b/assets/scripts/app/templates/profile/repo.hbs
@@ -0,0 +1,3 @@
+Settings: {{slug}}
+
+{{outlet}}
diff --git a/assets/scripts/app/templates/profile/tabs/hooks.hbs b/assets/scripts/app/templates/profile/tabs/hooks.hbs
index a937db01..8be19160 100644
--- a/assets/scripts/app/templates/profile/tabs/hooks.hbs
+++ b/assets/scripts/app/templates/profile/tabs/hooks.hbs
@@ -23,7 +23,7 @@
{{hook.description}}
-
+ {{#link-to "repo.settings" hook.repo class="repo-settings-icon tool-tip" title="Repository settings"}}{{/link-to}}
{{travis-switch action="toggle" target=hook}}
diff --git a/assets/scripts/app/templates/repo/settings.hbs b/assets/scripts/app/templates/repo/settings.hbs
new file mode 100644
index 00000000..1e515d4d
--- /dev/null
+++ b/assets/scripts/app/templates/repo/settings.hbs
@@ -0,0 +1,24 @@
+
+
Settings: {{#linkTo "repo" this}}{{slug}}{{/linkTo}}
+
+ {{#travis-tabs}}
+ {{#travis-tab "general" "General Settings"}}
+ {{#settings-form}}
+
+ Build only commits with .travis.yml file
+ {{travis-switch action="save" active=settings.builds_only_with_travis_yml}}
+
+
+
+ Build pushes
+ {{travis-switch action="save" active=settings.build_pushes}}
+
+
+
+ Build pull requests
+ {{travis-switch action="save" active=settings.build_pull_requests}}
+
+ {{/settings-form}}
+ {{/travis-tab}}
+ {{/travis-tabs}}
+
diff --git a/assets/scripts/app/templates/repos/show.hbs b/assets/scripts/app/templates/repos/show.hbs
index c1bf53b9..9b80786a 100644
--- a/assets/scripts/app/templates/repos/show.hbs
+++ b/assets/scripts/app/templates/repos/show.hbs
@@ -24,4 +24,3 @@
{{/if}}
{{/if}}
-
diff --git a/assets/scripts/app/templates/repos/show/tools.hbs b/assets/scripts/app/templates/repos/show/tools.hbs
index 5fc4135e..65f865b7 100644
--- a/assets/scripts/app/templates/repos/show/tools.hbs
+++ b/assets/scripts/app/templates/repos/show/tools.hbs
@@ -11,6 +11,12 @@
{{/if}}
+ {{#if view.displaySettingsLink}}
+
+ {{#linkTo "repo.settings" view.repo}}Settings{{/linkTo}}
+
+ {{/if}}
+