Permalink
Browse files

Design Builds, Staging preview, Harp, Dropbox WIP

Sincere apologies in advance for the monstrous commit. There were a lot of interconnected parts to this, and I didn't want to ship without at least a working local demo and a healthy group of tests.

I'll be doing more docs and a blog post soon, but here are the features added:
* Automatic staging version of the site at staging.yoursite.com
* Snapshots of theme builds, with rollback support
* Deploy design updates via Git/FTP (or soon Dropbox)
* Built in preprocessor support for Jade, CoffeeScript, Sass, Stylus, LESS, etc. for template/design assets (via Harp.js)
* Template editor can now edit most client files, like CSS, JS, and preprocessed files, in addition to Handlebars templates
* Templates/statics still served from local filesystem (fast)
* Use a symlink for the live build so updates are instant
* Editor files use a tree view for paths in names, command-enter now saves files in the Design Editor

While this sounds like overkill (and likely is, a bit), these features work together to provide a web design environment unlike any other CMS. The UI for all of these features is still light and straight-forward.
  • Loading branch information...
davidkaneda committed Oct 18, 2014
1 parent 08d0147 commit 31a9f2aee623e5f48532dc5758018e4aabb7bafc
Showing with 2,297 additions and 689 deletions.
  1. +4 −3 .gitignore
  2. +12 −6 Gruntfile.coffee
  3. +24 −15 client/source/controllers/templates_controller.coffee
  4. +5 −0 client/source/helpers.coffee
  5. +4 −2 client/source/helpers/forms.coffee
  6. +10 −0 client/source/models/build.coffee
  7. +7 −0 client/source/models/builds.coffee
  8. +3 −1 client/source/models/template.coffee
  9. +31 −1 client/source/models/templates.coffee
  10. +1 −1 client/source/routes.coffee
  11. +0 −4 client/source/templates/auth/login.hbs
  12. +1 −1 client/source/templates/buckets/dashboard.hbs
  13. +1 −1 client/source/templates/buckets/edit.hbs
  14. +1 −2 client/source/templates/entries/browser.hbs
  15. +5 −3 client/source/templates/layouts/loggedin.hbs
  16. +1 −1 client/source/templates/routes/list.hbs
  17. +21 −0 client/source/templates/templates/directory.hbs
  18. +62 −25 client/source/templates/templates/editor.hbs
  19. +17 −1 client/source/templates/users/edit.hbs
  20. +1 −1 client/source/templates/users/list.hbs
  21. +1 −10 client/source/views/layouts/loggedin.coffee
  22. +131 −27 client/source/views/templates/editor.coffee
  23. +20 −1 client/source/views/users/edit.coffee
  24. +4 −0 client/style/_typography.styl
  25. +37 −1 client/style/_views.styl
  26. +6 −4 client/style/bootstrap.less
  27. +33 −11 client/style/index.styl
  28. +16 −0 docs/user-docs/design-builds.md
  29. +6 −0 docs/user-docs/slugs.md
  30. +10 −0 package.json
  31. +6 −2 server/config.coffee
  32. +87 −13 server/index.coffee
  33. +5 −0 server/lib/database.coffee
  34. +18 −0 server/lib/logger.coffee
  35. +0 −1 server/lib/renderer.coffee
  36. +14 −12 {user/public → server/lib/skeletons/base}/css/index.css
  37. 0 {user/templates → server/lib/skeletons/base}/error.hbs
  38. +1 −1 {user/templates → server/lib/skeletons/base}/index.hbs
  39. +5 −5 {user/templates → server/lib/skeletons/base}/layout.hbs
  40. +0 −140 server/lib/template.coffee
  41. +0 −16 server/logger.coffee
  42. +344 −0 server/models/build.coffee
  43. +108 −0 server/models/buildfile.coffee
  44. +0 −40 server/models/template.coffee
  45. +146 −2 server/models/user.coffee
  46. +1 −0 server/routes/admin.coffee
  47. +89 −0 server/routes/api/buildfiles.coffee
  48. +112 −0 server/routes/api/builds.coffee
  49. +49 −0 server/routes/api/dropbox.coffee
  50. +3 −1 server/routes/api/index.coffee
  51. +4 −2 server/routes/api/install.coffee
  52. +0 −1 server/routes/api/management.coffee
  53. +0 −79 server/routes/api/templates.coffee
  54. +2 −2 server/routes/api/users.coffee
  55. +59 −24 server/routes/frontend.coffee
  56. +3 −1 server/views/admin.hbs
  57. +1 −1 test/auth.coffee
  58. +7 −2 test/reset.coffee
  59. +105 −0 test/server/integration/builds.coffee
  60. +1 −1 test/server/models/activity.coffee
  61. +100 −0 test/server/models/build.coffee
  62. +107 −0 test/server/models/buildfile.coffee
  63. +0 −1 test/server/models/entry.coffee
  64. +0 −2 test/server/models/user.coffee
  65. +19 −0 test/server/routes/admin.coffee
  66. +2 −2 test/server/routes/api/buckets.coffee
  67. +159 −0 test/server/routes/api/buildfiles.coffee
  68. +262 −0 test/server/routes/api/builds.coffee
  69. +3 −4 test/server/routes/api/install.coffee
  70. +0 −212 test/server/routes/api/templates.coffee
View
@@ -1,9 +1,10 @@
.DS_Store
+.env
+*.log
coverage.html
bower_components
-/public
+public
node_modules
tmp
docs/api
-newrelic_agent.log
-.env
+builds
View
@@ -1,5 +1,4 @@
mongoose = require 'mongoose'
-
module.exports = (grunt) ->
grunt.initConfig
pkg: grunt.file.readJSON 'package.json'
@@ -71,7 +70,7 @@ module.exports = (grunt) ->
clean:
app: ['public']
- all: ['public', 'bower_components', 'tmp']
+ all: ['public', 'bower_components', 'tmp', 'deployments/*', '!deployments/base']
testem:
basic:
@@ -125,9 +124,10 @@ module.exports = (grunt) ->
cwd: 'bower_components/ace-builds/src-min-noconflict/'
src: [
'ace.js'
- 'mode-handlebars.js'
- 'worker-handlebars.js'
+ 'mode-*.js'
+ 'worker-*.js'
'theme-*.js' # These are loaded on the fly anyway
+ 'ext-*.js' # These are loaded on the fly anyway
]
dest: 'public/js/ace/'
fontastic:
@@ -147,8 +147,12 @@ module.exports = (grunt) ->
dev:
options:
spawn: false
+ # background: false
script: 'server/start.coffee'
opts: ['node_modules/coffee-script/bin/coffee']
+ args: ['--debug-brk']
+ delay: 10
+ debug: yes
less:
app:
@@ -252,7 +256,7 @@ module.exports = (grunt) ->
tasks: ['copy']
style:
- files: ['client/style/**/*.{styl,less}']
+ files: ['client/style/**/*.{styl,less}', 'node_modules/buckets-*/**/*.styl']
tasks: ['build-style']
express:
@@ -268,7 +272,9 @@ module.exports = (grunt) ->
pluginStyles:
files: ['node_modules/buckets-*/**/*.styl']
- tasks: ['stylus:plugins', 'concat:plugins']
+ tasks: ['stylus:plugins', 'concat:pluginsStyle']
+ options:
+ livereload: true
livereload:
options:
@@ -1,35 +1,44 @@
Controller = require 'lib/controller'
Templates = require 'models/templates'
+Builds = require 'models/builds'
Template = require 'models/template'
TemplateEditor = require 'views/templates/editor'
module.exports = class TemplatesController extends Controller
edit: (params) ->
unless params.filename
- return @redirectTo 'templates#edit', filename: 'index'
+ return @redirectTo 'templates#edit', filename: 'index.hbs', env: 'staging'
@adjustTitle 'Templates'
@reuse 'Templates',
compose: ->
- @templates = new Templates
- @templates.fetch().done =>
- @template = @templates.findWhere filename: params.filename
-
- if params.filename and not @template
- toastr.warning 'That template was not found, starting a new file.'
- @template = new Template
- filename: params.filename
-
+ @builds = new Builds
+
+ @stagingFiles = new Templates
+ @liveFiles = new Templates
+ @liveFiles.build_env = 'live'
+ $.when(
+ @liveFiles.fetch()
+ @stagingFiles.fetch()
+ @builds.fetch()
+ ).done =>
@view = new TemplateEditor
- collection: @templates
- model: @template
+ stagingFiles: @stagingFiles
+ liveFiles: @liveFiles
+ builds: @builds
+ env: params.env
+ filename: params.filename
+
+ @view.selectTemplate params.filename, params.env
check: (options) ->
- if options.filename isnt @view.model.get('filename')
- @view.selectTemplate options.filename
- @view? and @templates?
+ if options.filename isnt @view.filename or options.env isnt @view.env
+ @view.selectTemplate options.filename, options.env
+
+ @view? and @stagingFiles? and @liveFiles? and @builds?
options:
filename: params.filename
+ env: params.env
@@ -83,3 +83,8 @@ Handlebars.registerHelper 'hasRole', (role..., options) ->
else
options.inverse @
+Handlebars.registerHelper 'startsWith', (string1, string2, options) ->
+ if string1?.indexOf(string2) is 0
+ options.fn @
+ else
+ options.inverse @
@@ -74,7 +74,9 @@ Handlebars.registerHelper 'input', (name, value, options) ->
placeholder: 'slug'
tabindex: 0
})
- input += slug
+ input += slug += """
+ <a href="/admin/help/slugs.md" class="btn btn-link btn-icon btn-icon-small">#{Handlebars.helpers.icon('question')}</a>
+ """
wrap input,
label: settings.label
@@ -101,7 +103,7 @@ Handlebars.registerHelper 'textarea', (name, value, options) ->
rows: settings.rows
, value
- if settings.label
+ new Handlebars.SafeString if settings.label
wrap textarea,
label: settings.label
help: settings.help
@@ -0,0 +1,10 @@
+Model = require 'lib/model'
+
+module.exports = class Build extends Model
+ urlRoot: '/api/builds/'
+ defaults:
+ env: 'staging'
+
+ checkDropbox: ->
+ @api '/api/builds/dropbox/check'
+ disconnectDropbox: ->
@@ -0,0 +1,7 @@
+Collection = require 'lib/collection'
+Build = require 'models/build'
+
+module.exports = class Builds extends Collection
+ url: '/api/builds'
+ model: Build
+ comparator: '-timestamp'
@@ -1,8 +1,10 @@
Model = require 'lib/model'
module.exports = class Template extends Model
- urlRoot: '/api/templates'
+ urlRoot: ->
+ "/api/buildfiles/#{@get('build_env')}/"
idAttribute: 'filename'
defaults:
filename: ''
contents: ''
+ build_env: 'staging'
@@ -1,7 +1,37 @@
+_ = require 'underscore'
+
Collection = require 'lib/collection'
Template = require 'models/template'
module.exports = class Templates extends Collection
- url: '/api/templates/'
+ build_env: 'staging'
+ url: ->
+ console.log 'fetching group', @build_env
+ "/api/buildfiles/#{@build_env}/"
model: Template
comparator: 'filename'
+ getTree: ->
+ # Converts list of paths (eg. from glob), to a tree structure
+ tree = {}
+
+ _.map @toJSON(), (obj) ->
+ parts = obj.filename.replace(/^\/|\/$/g, "").split('/')
+ ptr = tree
+ pathId = ""
+ path = ""
+ for part, i in parts
+ node =
+ name: part
+ type: 'directory'
+ pathId: pathId += "_#{part}".replace /[^A-Za-z0-9 \-\_]/, '-'
+ path: path += part + "/"
+
+ if i is parts.length - 1 and not obj.filename.match /\/$/
+ node.type = 'file'
+ node.path = obj.filename
+
+ ptr[part] = ptr[part] || node
+ ptr[part].children = ptr[part].children || {}
+ ptr = ptr[part].children
+
+ tree
@@ -22,7 +22,7 @@ module.exports = (match) ->
match 'buckets/:slug/:entryID', 'buckets#browse'
- match 'templates(/*filename)', 'templates#edit'
+ match 'design(/:env)(/*filename)', 'templates#edit'
match 'routes', 'routes#list'
match 'help(/*doc)', 'help#index'
@@ -1,7 +1,6 @@
<form class="container-fluid" action="/{{adminSegment}}/login" method="POST">
<div class="row">
<div class="col-sm-4 col-sm-offset-4">
-
<header id="bkts-hdr">
{{logo}}
</header>
@@ -15,13 +14,10 @@
<h3>Please log in{{#if next}} to continue{{/if}}:</h3>
{{input 'username' '' autocomplete="on" placeholder="Email" type='email' size='lg'}}
{{input 'password' '' autocomplete="on" placeholder="Password" type='password' size='lg'}}
-
{{submit 'Log in' className='btn btn-lg btn-block btn-primary ladda-button'}}
<p><br><br><a href="#forgot">Forget your password?</a></p>
</fieldset>
-
</div>
</div>
-
</form>
@@ -1,5 +1,5 @@
<div class="row">
- <h1 class="page-title background-yellow">Buckets</h1>
+ <h1 class="page-title color-yellow">Buckets</h1>
<div class="col-md-4 col-sm-6">
<div class="panel">
@@ -1,5 +1,5 @@
<form class="row">
- <h1 class="page-title background-{{color}}">
+ <h1 class="page-title">
{{#if id}}
<a href="#delete" class="btn btn-link btn-icon pull-right">{{icon 'trash'}}</a>
Edit Bucket
@@ -1,5 +1,5 @@
<div class="row">
- <h1 class="page-title background-{{bucket.color}}">
+ <h1 class="page-title color-{{bucket.color}}">
{{bucket.name}}
<span class="hasEntries pull-right {{#empty items}}hidden{{/empty}}">
<a class="btn btn-link btn-icon btn-icon-small show-tooltip" title="Add {{bucket.singular}}" href="/{{adminSegment}}/buckets/{{bucket.slug}}/add">{{icon 'plus'}}</a>
@@ -9,7 +9,6 @@
{{/hasRole}}
</h1>
<div class="col-md-5 col-sm-4 sidebar">
- <br>
<div class="entries"></div>
</div>
<div class="col-md-7 col-sm-8 entry-detail">
@@ -29,7 +29,7 @@
{{#hasRole 'administrator'}}
<li class="divider"></li>
<li><a href="/{{adminSegment}}/routes">{{icon 'direction-sign'}} Routes</a></li>
- <li><a href="/{{adminSegment}}/templates">{{icon 'html'}} Templates</a></li>
+ <li><a href="/{{adminSegment}}/design/">{{icon 'html'}} Design</a></li>
<li class="divider"></li>
{{!-- <li><a href="/{{adminSegment}}/settings">{{icon 'setting-gear'}} Settings</a></li> --}}
<li><a href="/{{adminSegment}}/users">{{icon 'user'}} People</a></li>
@@ -39,13 +39,15 @@
<a href="#" data-toggle="dropdown">
{{gravatar email_hash}} <span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
+
+ {{name}}
</a>
<ul class="dropdown-menu" role="menu">
<li class="logout">
- <a href="/" class="noscript">Return to website</a>
+ <a class="noscript" href="/{{adminSegment}}/logout">Log&nbsp;out</a>
<a href="/{{adminSegment}}/docs/api/" target="_blank">API Docs</a>
<a href="/{{adminSegment}}/users/{{email}}">Profile</a>
- <a class="noscript" href="/{{adminSegment}}/logout">Log&nbsp;out</a>
+ <a href="/" class="noscript">Return to website</a>
</li>
</ul>
</li>
@@ -1,5 +1,5 @@
<div class="row">
- <h1 class="page-title background-yellow">
+ <h1 class="page-title color-yellow">
Routes
<div class="pull-right">
{{helpIcon 'More about URL patterns' docsPath="routes.md"}}
@@ -0,0 +1,21 @@
+{{debug}}
+
+{{#each children}}
+ {{#is type 'file'}}
+
+ <li{{#is path ../../active}} class="active"{{/is}} data-env="{{../../env}}" data-path="{{path}}">
+ <a href="/{{adminSegment}}/design/{{../../env}}/{{path}}">{{name}}</a>
+ <div class="input-group-btn">{{../../env}}
+ <a href="#deleteFile" class="btn btn-link btn-icon btn-icon-small">{{icon 'trash'}}</a>
+ </div>
+ </li>
+ {{else}}
+
+ <li>
+ <h4 data-toggle="collapse" data-target="#collapse-{{../../env}}{{pathId}}"{{#startsWith ../../active path}}{{else}} class="collapsed"{{/startsWith}}><span class="caret"></span> {{name}}</h4>
+ <ul class="collapse{{#startsWith ../../active path}} in{{/startsWith}}" id="collapse-{{../../env}}{{pathId}}">
+ {{> directory children=children env=../../env active=../../active}}
+ </ul>
+ </li>
+ {{/is}}
+{{/each}}
Oops, something went wrong.

0 comments on commit 31a9f2a

Please sign in to comment.