Settings pane

This commit contains a settings pane implementation. There are a couple
of things here, which are not used yet, like advanced form helpers. I'm
leaving them here, because the plan is to add support for more settings
soon (like: include/exclude branch patterns), which will need these
helpers.

There is also tabs support, although in the current version there is
only one tab (initially it was created for supporting general tab and
notifications tab).
This commit is contained in:
Piotr Sarnacki 2013-10-01 10:22:54 +02:00
parent eb3ec57be1
commit c18222ea51
13 changed files with 419 additions and 15 deletions

View File

@ -74,7 +74,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) ->

View File

@ -48,6 +48,24 @@ Travis.FirstSyncController = Em.Controller.extend
isSyncing: Ember.computed.alias('user.isSyncing')
Travis.ProfileRepoController = Em.ObjectController.extend()
Travis.ProfileRepoSettingsTabController = Em.ObjectController.extend()
Travis.ProfileRepoSettingsController = Em.Controller.extend
needs: ['profileRepoSettingsTab', 'profileRepo']
tab: Ember.computed.alias('controllers.profileRepoSettingsTab.model.tab')
repo: Ember.computed.alias('controllers.profileRepo.content')
submit: ->
@set('saving', true)
self = this
@get('repo').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'

View File

@ -3,6 +3,292 @@ 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')
layout: Ember.Handlebars.compile(
'<ul class="tabs">' +
' {{#each tab in tabs}}' +
' <li {{bindAttr class="tab.visible:active"}}>' +
' <h5>{{#linkTo "profile.repo.settings.tab" tab.id}}{{tab.name}}{{/linkTo}}</h5>' +
' </li>' +
' {{/each}}' +
'</ul>' +
'{{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 '<span class="tool-tip" original-title="' + tip + '">' + text + '</span>'

View File

@ -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()

View File

@ -27,10 +27,10 @@ Ember.Route.reopen
authController.set('redirected', true)
@transitionTo('auth')
else
throw(error)
@_super(error)
renderNoOwnedRepos: ->
@render('no_owned_repos', outlet: 'main')
@render('no_owned_repos')
renderFirstSync: ->
@renderFirstSync()
@ -41,6 +41,7 @@ Ember.Route.reopen
afterSignOut: ->
@afterSignOut()
afterSignIn: ->
if transition = Travis.auth.get('afterSignInTransition')
Travis.auth.set('afterSignInTransition', null)
@ -86,6 +87,7 @@ Travis.Router.reopen
@_super.apply this, arguments
Travis.Router.map ->
@resource 'index', path: '/', ->
@route 'current', path: '/'
@ -103,6 +105,10 @@ Travis.Router.map ->
@route 'auth', path: '/auth'
@route 'notFound', path: '/not-found'
@resource 'profile.repo', path: '/profile/:owner/:name', ->
@resource 'profile.repo.settings', path: 'settings', ->
@route 'tab', path: ':tab'
@resource 'profile', path: '/profile', ->
@route 'index', path: '/'
@resource 'account', path: '/:login', ->
@ -125,7 +131,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')
Travis.GettingStartedRoute = Ember.Route.extend
setupController: ->
@ -158,7 +164,7 @@ Travis.FirstSyncRoute = Ember.Route.extend
Travis.IndexCurrentRoute = Ember.Route.extend Travis.SetupLastBuild,
renderTemplate: ->
@render 'repo'
@render 'build', outlet: 'pane', into: 'repo'
@render 'build', into: 'repo'
setupController: ->
@_super.apply this, arguments
@ -175,7 +181,7 @@ Travis.IndexCurrentRoute = Ember.Route.extend Travis.SetupLastBuild,
Travis.AbstractBuildsRoute = Ember.Route.extend
renderTemplate: ->
@render 'builds', outlet: 'pane', into: 'repo'
@render 'builds', into: 'repo'
setupController: ->
@controllerFor('repo').activate(@get('contentType'))
@ -200,7 +206,7 @@ Travis.BranchesRoute = Travis.AbstractBuildsRoute.extend(contentType: 'branches'
Travis.BuildRoute = Ember.Route.extend
renderTemplate: ->
@render 'build', outlet: 'pane', into: 'repo'
@render 'build', into: 'repo'
serialize: (model, params) ->
id = if model.get then model.get('id') else model
@ -221,7 +227,7 @@ Travis.BuildRoute = Ember.Route.extend
Travis.JobRoute = Ember.Route.extend
renderTemplate: ->
@render 'job', outlet: 'pane', into: 'repo'
@render 'job', into: 'repo'
serialize: (model, params) ->
id = if model.get then model.get('id') else model
@ -248,7 +254,7 @@ Travis.RepoIndexRoute = Ember.Route.extend Travis.SetupLastBuild,
@controllerFor('repo').activate('current')
renderTemplate: ->
@render 'build', outlet: 'pane', into: 'repo'
@render 'build', into: 'repo'
Travis.RepoRoute = Ember.Route.extend
renderTemplate: ->
@ -267,7 +273,6 @@ Travis.RepoRoute = Ember.Route.extend
model: (params) ->
slug = "#{params.owner}/#{params.name}"
Travis.Repo.fetchBySlug(slug)
actions:
@ -324,7 +329,7 @@ Travis.ProfileRoute = Ember.Route.extend
@render 'top', outlet: 'top'
@render 'accounts', outlet: 'left'
@render 'flash', outlet: 'flash'
@render 'profile'
@_super.apply(this, arguments)
Travis.ProfileIndexRoute = Ember.Route.extend
setupController: ->
@ -392,3 +397,29 @@ Travis.AuthRoute = Ember.Route.extend
deactivate: ->
@controllerFor('auth').set('redirected', false)
Travis.ProfileRepoRoute = Travis.ProfileRoute.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)
controller.set('content', 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)
Travis.ProfileRepoSettingsRoute = Ember.Route.extend
setupController: (controller, model) ->
controller.set('settings', model)
model: ->
repo = @modelFor('profileRepo')
repo.fetchSettings()

View File

@ -8,7 +8,7 @@
<div id="main">
{{outlet flash}}
{{outlet main}}
{{outlet}}
</div>
<div id="right">

View File

@ -0,0 +1,3 @@
<h1>Settings: {{slug}}</h1>
{{outlet}}

View File

@ -0,0 +1,29 @@
{{#travis-tabs}}
{{#travis-tab "general" "General Settings"}}
{{#settings-form}}
<p>
{{settings-input checked=builds_only_with_travis_yml type="checkbox" id="builds-only-with-travis-yml"}}
<label for="builds-only-with-travis-yml">Build only commits with .travis.yml file</label>
</p>
<p>
{{settings-input checked=build_pushes type="checkbox" id="build-pushes"}}
<label for="build-pushes">Build pushes</label>
</p>
<p>
{{settings-input checked=build_pull_requests type="checkbox" id="build-pull-requests"}}
<label for="build-pull-requests">Build pull requests</label>
</p>
<p>
{{#if saving}}
<span class="saving">Saving</span>
{{else}}
<button type="submit">Submit</button>
{{/if}}
</p>
{{/settings-form}}
{{/travis-tab}}
{{/travis-tabs}}

View File

@ -17,7 +17,7 @@
{{/with}}
<div class="tab">
{{outlet pane}}
{{outlet}}
</div>
{{else}}
<div class="loading"><span>Loading</span></div>

View File

@ -45,6 +45,7 @@ Travis.FirstSyncView = Travis.View.extend
)
, Travis.config.syncingPageRedirectionTime
require 'views/accounts'
require 'views/application'
require 'views/build'

View File

@ -23,13 +23,14 @@ Travis.ajax = Em.Object.create
!result
ajax: (url, method, options) ->
method = method || "GET"
method = method.toUpperCase()
endpoint = Travis.config.api_endpoint || ''
options = options || {}
token = Travis.sessionStorage.getItem('travis.token')
if token && Travis.ajax.needsAuth(method, url)
if token && (Travis.ajax.needsAuth(method, url) || options.forceAuth)
options.headers ||= {}
options.headers['Authorization'] ||= "token #{token}"

View File

@ -0,0 +1,28 @@
view = null
module "Handlebars helpers - settings-input",
setup: ->
Ember.run -> Travis.advanceReadiness()
test "settings input allows to bind to nested objects", ->
controller = Ember.Object.create()
view = Ember.View.create(
controller: controller
template: Ember.Handlebars.compile("{{input value=foo}} {{controller}}")
)
Ember.run ->
view.append()
Ember.run ->
controller.set('foo', 'bar')
Ember.run.sync()
input = view.$('input')
input.val('a value').change()
Ember.run.sync()
console.log controller.get('foo')

View File

@ -11,7 +11,7 @@
.loading
display: none
span.loading
span.loading, span.saving
padding: 0 25px 0 0
font-size: $font-size-small
color: $color-text-lighter