From c18222ea51993d594c71ea191fd443d4cf274122 Mon Sep 17 00:00:00 2001 From: Piotr Sarnacki Date: Tue, 1 Oct 2013 10:22:54 +0200 Subject: [PATCH] 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). --- assets/scripts/app/auth.coffee | 2 +- assets/scripts/app/controllers.coffee | 18 ++ assets/scripts/app/helpers/handlebars.coffee | 286 ++++++++++++++++++ assets/scripts/app/models/repo.coffee | 7 + assets/scripts/app/routes.coffee | 51 +++- .../scripts/app/templates/layouts/profile.hbs | 2 +- assets/scripts/app/templates/profile/repo.hbs | 3 + .../app/templates/profile/repo/settings.hbs | 29 ++ assets/scripts/app/templates/repos/show.hbs | 2 +- assets/scripts/app/views.coffee | 1 + assets/scripts/lib/travis/ajax.coffee | 3 +- .../spec/unit/settings_helpers_spec.coffee | 28 ++ assets/styles/app/loading.sass | 2 +- 13 files changed, 419 insertions(+), 15 deletions(-) create mode 100644 assets/scripts/app/templates/profile/repo.hbs create mode 100644 assets/scripts/app/templates/profile/repo/settings.hbs create mode 100644 assets/scripts/spec/unit/settings_helpers_spec.coffee diff --git a/assets/scripts/app/auth.coffee b/assets/scripts/app/auth.coffee index 565a18c1..b8116df7 100644 --- a/assets/scripts/app/auth.coffee +++ b/assets/scripts/app/auth.coffee @@ -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) -> diff --git a/assets/scripts/app/controllers.coffee b/assets/scripts/app/controllers.coffee index f1b4f520..bcec80fd 100644 --- a/assets/scripts/app/controllers.coffee +++ b/assets/scripts/app/controllers.coffee @@ -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' diff --git a/assets/scripts/app/helpers/handlebars.coffee b/assets/scripts/app/helpers/handlebars.coffee index 8dfda033..63e6cc15 100644 --- a/assets/scripts/app/helpers/handlebars.coffee +++ b/assets/scripts/app/helpers/handlebars.coffee @@ -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( + '' + + '{{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/repo.coffee b/assets/scripts/app/models/repo.coffee index f582fb10..ca098348 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 0f8186c1..ba390fb9 100644 --- a/assets/scripts/app/routes.coffee +++ b/assets/scripts/app/routes.coffee @@ -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() diff --git a/assets/scripts/app/templates/layouts/profile.hbs b/assets/scripts/app/templates/layouts/profile.hbs index 6ce4e370..e149cc94 100644 --- a/assets/scripts/app/templates/layouts/profile.hbs +++ b/assets/scripts/app/templates/layouts/profile.hbs @@ -8,7 +8,7 @@
{{outlet flash}} - {{outlet main}} + {{outlet}}