diff options
author | Paul Duncan <pabs@pablotron.org> | 2016-05-24 12:00:43 -0400 |
---|---|---|
committer | Paul Duncan <pabs@pablotron.org> | 2016-05-24 12:00:43 -0400 |
commit | c758a064eb97b16fd875c9f5a624bb98f8a774dc (patch) | |
tree | 7142b85e14d98ae23a0a25a781d1104fdb91a27b | |
parent | 0cfd477f80c3773e97ec5b9a19c13e0aaf229d9f (diff) | |
download | guff-c758a064eb97b16fd875c9f5a624bb98f8a774dc.tar.bz2 guff-c758a064eb97b16fd875c9f5a624bb98f8a774dc.zip |
add full editing, fts search
-rw-r--r-- | data/assets/js/admin/dialogs/blog-edit.js | 20 | ||||
-rw-r--r-- | data/assets/js/admin/dialogs/page-edit.js | 23 | ||||
-rw-r--r-- | data/assets/js/admin/dialogs/post-edit.js | 88 | ||||
-rw-r--r-- | data/assets/js/admin/dialogs/project-edit.js | 21 | ||||
-rw-r--r-- | data/assets/js/admin/tabs/posts.js | 124 | ||||
-rw-r--r-- | src/guff.cr | 196 | ||||
-rw-r--r-- | src/views/admin-page.ecr | 72 |
7 files changed, 359 insertions, 185 deletions
diff --git a/data/assets/js/admin/dialogs/blog-edit.js b/data/assets/js/admin/dialogs/blog-edit.js index 46a782b..0084028 100644 --- a/data/assets/js/admin/dialogs/blog-edit.js +++ b/data/assets/js/admin/dialogs/blog-edit.js @@ -1,7 +1,23 @@ jQuery(function($) { "use strict"; - $('#blog-edit-dialog').on('post-data-loaded', function(r) { - console.log(r); + var p = '#blog-edit-'; + + $(p + 'dialog').on('guff.loaded', function(ev) { + var r = ev.post_data; + $(p + 'tags').val(r.tags); + }); + + $(p + 'confirm').click(function() { + $(p + 'dialog').trigger({ + type: 'guff.save', + + post_data: { + tags: $(p + 'tags').val(), + }, + }); + + // stop event + return false; }); }); diff --git a/data/assets/js/admin/dialogs/page-edit.js b/data/assets/js/admin/dialogs/page-edit.js index 8af9ec2..05d69a7 100644 --- a/data/assets/js/admin/dialogs/page-edit.js +++ b/data/assets/js/admin/dialogs/page-edit.js @@ -1,7 +1,26 @@ jQuery(function($) { "use strict"; - $('#page-edit-dialog').on('post-data-loaded', function(r) { - console.log(r); + var p = '#page-edit-'; + + $(p + 'dialog').on('guff.loaded', function(ev) { + var r = ev.post_data; + + $(p + 'layout a').removeClass('btn-primary').addClass('btn-default'); + $(p + 'layout a[data-val="' + r.layout + '"]') + .toggleClass('btn-primary btn-default'); + }); + + $(p + 'confirm').click(function() { + $(p + 'dialog').trigger({ + type: 'guff.save', + + post_data: { + layout: $(p + 'layout .btn-primary').data('val'), + }, + }); + + // stop event + return false; }); }); diff --git a/data/assets/js/admin/dialogs/post-edit.js b/data/assets/js/admin/dialogs/post-edit.js index 31b85ee..01baba1 100644 --- a/data/assets/js/admin/dialogs/post-edit.js +++ b/data/assets/js/admin/dialogs/post-edit.js @@ -8,7 +8,7 @@ jQuery(function($) { if (slug.prop('disabled')) { slug.val( name.val().toLowerCase() - .replace(/[^a-z0-9_\.-]+/g, '-') + .replace(/[^a-z0-9_-]+/g, '-') .replace(/^-+|-+$/g, '') ); } @@ -17,9 +17,39 @@ jQuery(function($) { $.each(['blog', 'page', 'project'], function(i, id) { var p = '#' + id + '-edit-'; - $(p + 'dialog').one('shown.bs.modal', function() { - // lazy-init editor - CKEDITOR.replace(id + '-edit-body'); + $(p + 'dialog').on('guff.loaded', function(ev) { + var r = ev.post_data, + slug_lock = (r.slug_lock == "1"); + + $(p + 'name').val(r.name); + $(p + 'slug').val(r.slug) + .prop('disabled', slug_lock ? 'disabled' : null); + + $(p + 'slug-lock') + .toggleClass('btn-default', slug_lock) + .toggleClass('btn-primary', !slug_lock) + .find('fa') + .toggleClass('fa-lock', slug_lock) + .toggleClass('fa-unlock', !slug_lock); + + var editor = CKEDITOR.instances[id + '-edit-body']; + if (!editor) { + // lazy-init editor + editor = CKEDITOR.replace(id + '-edit-body'); + } + + if (editor.status == 'ready') { + // editor is ready, set body data + editor.setData(r.body); + } else { + editor.once('instanceReady', function() { + editor.setData(r.body); + }); + } + + $(p + 'state a').removeClass('btn-primary').addClass('btn-default'); + $(p + 'state a[data-val="' + r.state + '"]') + .toggleClass('btn-primary btn-default'); }).on('show.bs.modal', function() { var me = $(this); @@ -50,7 +80,7 @@ jQuery(function($) { me.find('.modal-body.loading-done').removeClass('hidden'); me.trigger({ - type: "post-data-loaded", + type: "guff.loaded", post_data: r, }); }); @@ -61,21 +91,40 @@ jQuery(function($) { $(this).data('close-dialog-confirmed') || confirm('Close without saving changes?') ); - }).find('button[data-dismiss="modal"]').click(function() { - // override close confirmation - // FIXME: should this only be on save? - $(p + 'dialog').data('close-dialog-confirmed', true); - }); + }).on('guff.save', function(ev) { + $(p + 'confirm').addClass('disabled') + .find('.loading').toggleClass('hidden'); + + send(id + "/set", $.extend({ + post_id: $(p + 'dialog').data('post_id'), + name: $(p + 'name').val(), + slug_lock: $(p + 'slug-lock').hasClass('btn-default') ? 't' : 'f', + slug: $(p + 'slug').val(), + body: CKEDITOR.instances[id + '-edit-body'].getData(), + state: $(p + 'state .btn-primary').data('val'), + }, ev.post_data)).always(function() { + $(p + 'confirm').removeClass('disabled') + .find('.loading').toggleClass('hidden'); + }).fail(function(r) { + var error = r.responseText; - $(p + 'confirm').click(function() { - if ($(this).hasClass('disabled')) - return false; + try { + var data = $.parseJSON(r.responseText); + if (data.error) + error = data.error; + } catch (e) {} - // TODO: see #user-add-confirm - alert('TODO: create'); + alert('Error: ' + error); + }).done(function(r) { + // reload posts + $('#posts-reload').click(); - // stop event - return false; + // dismiss dialog + $(p + 'dialog').data('close-dialog-confirmed', true).modal('hide'); + }); + }).find('button[data-dismiss="modal"]').click(function() { + // override close confirmation dialog + $(p + 'dialog').data('close-dialog-confirmed', true); }); }); @@ -109,8 +158,9 @@ jQuery(function($) { return false; }); - $('.state-buttons').on('a', 'click', function() { - $(this).parent().find('.btn-primary').toggleClass('btn-default btn-primary'); + $('.state-buttons a').click(function() { + $(this).parent().find('.btn-primary') + .toggleClass('btn-default btn-primary'); $(this).toggleClass('btn-default btn-primary'); // stop event diff --git a/data/assets/js/admin/dialogs/project-edit.js b/data/assets/js/admin/dialogs/project-edit.js index a31abf2..17236f6 100644 --- a/data/assets/js/admin/dialogs/project-edit.js +++ b/data/assets/js/admin/dialogs/project-edit.js @@ -1,7 +1,24 @@ jQuery(function($) { "use strict"; - $('#project-edit-dialog').on('post-data-loaded', function(r) { - console.log(r); + var p = '#project-edit-'; + + $(p + 'dialog').on('guff.loaded', function(ev) { + var r = ev.post_data; + + $(p + 'repo').val(r.repo_url); + }); + + $(p + 'confirm').click(function() { + $(p + 'dialog').trigger({ + type: 'guff.save', + + post_data: { + repo_url: $(p + 'repo').val(), + }, + }); + + // stop event + return false; }); }); diff --git a/data/assets/js/admin/tabs/posts.js b/data/assets/js/admin/tabs/posts.js index 00117d6..03f3483 100644 --- a/data/assets/js/admin/tabs/posts.js +++ b/data/assets/js/admin/tabs/posts.js @@ -2,122 +2,30 @@ jQuery(function($) { "use strict"; var TEMPLATES = new LuigiTemplate.Cache({ - user: [ - "<a ", - "href='#' ", - "class='list-group-item %{css|h}' ", - "title='Edit user \"%{user_name|h}\".' ", - "data-row='%{row|json|h}' ", - "data-q='%{q|h}' ", - ">", - "<i class='fa fa-fw fa-spinner fa-spin hidden loading'></i>", - "<i class='fa fa-fw fa-user loading'></i>", - " ", - "%{user_name|h} (%{email|h})", - - "<span class='badge pull-right'>", - "%{role_name|h}", - "</span>", - "</a>", - ], - - loading: [ - "<span class='list-group-item disabled'>", - "<i class='fa fa-spinner fa-spin'></i>", - " ", - "Loading...", - "</span>", - ], - - error: [ - "<span class='list-group-item list-group-item-danger disabled'>", - "<i class='fa fa-exclamation-triangle'></i>", - " ", - "Error: %{responseText|h}", - "</span>", - ], }); - function update_slug(name, slug) { - if (slug.prop('disabled')) { - slug.val( - name.val().toLowerCase() - .replace(/[^a-z0-9_\.-]+/g, '-') - .replace(/^-+|-+$/g, '') - ); - } - } + $('.add-post').click(function() { + var type = $(this).data('type'); - $.each(['blog', 'page', 'project'], function(i, id) { - var p = '#' + id + '-edit-'; + // dismiss dropdown + $('body').trigger('click'); - $(p + 'dialog').one('shown.bs.modal', function() { - // lazy-init editor - CKEDITOR.replace(id + '-edit-body'); - }).on('show.bs.modal', function() { - // reset close confirmation - $(this).data('close-dialog-confirmed', false); - - // hide all bodies - $(this).find('.modal-body').addClass('hidden'); - }).on('shown.bs.modal', function() { - $(p + 'name').focus(); - }).on('hide.bs.modal', function() { - return ( - $(this).data('close-dialog-confirmed') || - confirm('Close without saving changes?') - ); - }).find('button[data-dismiss="modal"]').click(function() { - // override close confirmation - // FIXME: should this only be on save? - $(p + 'dialog').data('close-dialog-confirmed', true); - }); + send(type + '/add').fail(function(r) { + var msg = r.responseText; - $(p + 'confirm').click(function() { - if ($(this).hasClass('disabled')) - return false; + try { + var data = $.parseJSON(r); + if (data.error) + msg = data.error; + } catch (e) {} - // TODO: see #user-add-confirm - alert('TODO: create'); + alert('Error: ' + msg); + }).done(function(r) { + console.log(r); - // stop event - return false; + // show edit dialog + $('#' + type + '-edit-dialog').data('post_id', r.post_id).modal('show') }); - }); - - $('.post-name').keydown(function() { - var name = $(this), - slug = $(this).parents('.modal-body').find('.post-slug'); - - setTimeout(function() { - update_slug(name, slug); - }, 10); - }); - - $('.post-slug-lock').click(function() { - var modal_body = $(this).parents('.modal-body'); - - // toggle locked state - $(this).toggleClass('btn-default btn-primary') - .find('.fa').toggleClass('fa-lock fa-unlock'); - var locked = $(this).hasClass('btn-default'); - - // update slug disabled state - var slug = modal_body.find('.post-slug'); - slug.prop('disabled', locked ? 'disabled' : null); - - if (locked) { - // auto-generate slug - update_slug(modal_body.find('.post-name'), slug); - } - - // stop event - return false; - }); - - $('.state-buttons').on('a', 'click', function() { - $(this).parent().find('.btn-primary').toggleClass('btn-default btn-primary'); - $(this).toggleClass('btn-default btn-primary'); // stop event return false; diff --git a/src/guff.cr b/src/guff.cr index b082b4e..6367671 100644 --- a/src/guff.cr +++ b/src/guff.cr @@ -215,6 +215,28 @@ module Guff end end + class PagedResultSet + def initialize( + @page : Int32, + @num_rows : Int64, + @limit : Int32, + @rows : Array(Hash(String, String)) + ) + end + + def to_json(io) + { + "meta": { + "page": @page, + "num_rows": @num_rows, + "num_pages": (1.0 * @num_rows / @limit).ceil + }, + + "rows": @rows + }.to_json(io) + end + end + module Models abstract class Model def initialize(@context : Context) @@ -228,12 +250,23 @@ module Guff (?, ?, (SELECT state_id FROM states WHERE state = 'draft')) ", + fts_add: " + INSERT INTO posts_fts(rowid, name, slug, body) + SELECT post_id, name, slug, body FROM posts WHERE post_id = ? + ", + set: " UPDATE posts SET %s WHERE post_id = ? ", + fts_set: " + UPDATE posts_fts + SET %s + WHERE rowid = ? + ", + count_posts: " SELECT COUNT(*) @@ -290,8 +323,20 @@ module Guff site_id : Int64, user_id : Int64, ) : Int64 - @context.dbs.rw.query(SQL[:add], [site_id.to_s, user_id.to_s]) - @context.dbs.rw.last_insert_row_id.to_i64 + db = @context.dbs.rw + post_id = -1_i64 + + db.transaction do + # add entry + db.query(SQL[:add], [site_id.to_s, user_id.to_s]) + post_id = db.last_insert_row_id.to_i64 + + # populate fts index + db.query(SQL[:fts_add], [post_id.to_s]) + end + + # return post_id + post_id end def set( @@ -314,6 +359,8 @@ module Guff ) sets = [] of String args = [] of String + fts_sets = [] of String + fts_args = [] of String if site_id sets << "site_id = ?" @@ -351,6 +398,9 @@ module Guff if slug sets << "slug = ?" args << slug + + fts_sets << "slug = ?" + fts_args << slug end unless slug_lock.nil? @@ -361,16 +411,29 @@ module Guff if name sets << "name = ?" args << name + + fts_sets << "name = ?" + fts_args << name end if body sets << "body = ?" args << body + + fts_sets << "body = ?" + fts_args << body.gsub(/<.+?>/, " ") end if sets.size > 0 + # update posts args << post_id.to_s @context.dbs.rw.query(SQL[:set] % sets.join(","), args) + + if fts_sets.size > 0 + # update posts fts + fts_args << post_id.to_s + @context.dbs.rw.query(SQL[:fts_set] % fts_sets.join(","), fts_args) + end end end @@ -380,6 +443,7 @@ module Guff site_id : Int64? = nil, post_type : String? = nil, state : String? = nil, + q : String? = nil, page : Int32 = 1, ) filters = %w{1} @@ -389,11 +453,13 @@ module Guff raise "invalid page: #{page}" unless page > 0 if site_id + # add site filter filters << "a.site_id = ?" args << site_id.to_s end if post_type + # add type filter filters << case post_type when "blog" "c.post_id IS NOT NULL" @@ -407,12 +473,25 @@ module Guff end if state + # add state filter filters << "b.state = ?" args << state else + # default state filter filters << "b.state IN ('draft', 'posted')" end + if q && q.match(/\S+/) + # add search filter + filters << "a.post_id IN ( + SELECT rowid + FROM posts_fts + WHERE posts_fts MATCH ? + )" + + args << q + end + # build where clause filter_sql = filters.join(" AND ") @@ -441,15 +520,12 @@ module Guff end # return results - { - "meta": { - "page": page, - "num_posts": num_posts, - "num_pages": (1.0 * num_posts / LIMIT).ceil - }, - - "rows": rows, - } + PagedResultSet.new( + page: page, + num_rows: num_posts, + limit: LIMIT, + rows: rows, + ) end end @@ -581,7 +657,7 @@ module Guff end def get(post_id : Int64) - @context.dbs.ro.row(SQL[:get], [post_id.to_s]) + @context.dbs.ro.row(SQL[:get], [post_id.to_s]).not_nil! end end @@ -710,7 +786,7 @@ module Guff end def get(post_id : Int64) - @context.dbs.ro.row(SQL[:get], [post_id.to_s]) + @context.dbs.ro.row(SQL[:get], [post_id.to_s]).not_nil! end end @@ -876,7 +952,7 @@ module Guff end def get(post_id : Int64) - @context.dbs.ro.row(SQL[:get], [post_id.to_s]) + @context.dbs.ro.row(SQL[:get], [post_id.to_s]).not_nil! end end @@ -1138,12 +1214,25 @@ module Guff LIMIT 1 ", + + get_default_id: " + SELECT site_id + + FROM sites + + WHERE is_active + AND is_default + " } def get_id(host : String?) : Int64? r = @context.dbs.ro.one(SQL[:get_id], [host || ""]) r ? r.to_i64 : nil end + + def get_default_id : Int64 + @context.dbs.ro.one(SQL[:get_default_id]).not_nil!.to_i64 + end end class RoleModel < Model @@ -1317,6 +1406,7 @@ module Guff site_id: params["site_id"]? ? params["site_id"].to_i64 : nil, state: params["state"]?, post_type: params["post_type"]?, + q: params["q"]?, page: params["page"].to_i32, ) end @@ -1325,7 +1415,7 @@ module Guff module PageAPI def do_page_add(params : HTTP::Params) post_id = @context.models.page.add( - site_id: params["site_id"].to_i64, + site_id: params["site_id"]? ? params["site_id"].to_i64 : @context.models.site.get_default_id, user_id: @context.user_id.not_nil!, ) @@ -1356,12 +1446,21 @@ module Guff nil end + + def do_page_get(params : HTTP::Params) + @context.models.page.get( + post_id: params["post_id"].to_i64 + ).reduce({} of String => String) do |r, k, v| + r[k] = v.to_s + r + end + end end module ProjectAPI def do_project_add(params : HTTP::Params) post_id = @context.models.project.add( - site_id: params["site_id"].to_i64, + site_id: params["site_id"]? ? params["site_id"].to_i64 : @context.models.site.get_default_id, user_id: @context.user_id.not_nil!, ) @@ -1392,12 +1491,21 @@ module Guff nil end + + def do_project_get(params : HTTP::Params) + @context.models.project.get( + post_id: params["post_id"].to_i64 + ).reduce({} of String => String) do |r, k, v| + r[k] = v.to_s + r + end + end end module BlogAPI def do_blog_add(params : HTTP::Params) post_id = @context.models.blog.add( - site_id: params["site_id"].to_i64, + site_id: params["site_id"]? ? params["site_id"].to_i64 : @context.models.site.get_default_id, user_id: @context.user_id.not_nil!, ) @@ -1426,6 +1534,15 @@ module Guff nil end + + def do_blog_get(params : HTTP::Params) + @context.models.blog.get( + post_id: params["post_id"].to_i64 + ).reduce({} of String => String) do |r, k, v| + r[k] = v.to_s + r + end + end end module UserAPI @@ -1566,10 +1683,9 @@ module Guff new_post_button: " <a href='#' - class='btn btn-primary' + class='btn btn-primary add-post' title='Create new blog post.' - data-toggle='modal' - data-target='#blog-edit-dialog' + data-type='blog' > <i class='fa fa-plus-circle'></i> New Post @@ -1581,33 +1697,16 @@ module Guff title='Show additonal options.' data-toggle='dropdown' > - <span class='hidden'> - <i class='fa fa-plus-circle'></i> - New - </span> - <i class='fa fa-caret-down'></i> </a> <ul class='dropdown-menu'> - <li class='hidden'> - <a - href='#' - title='Create new blog post.' - data-toggle='modal' - data-target='#blog-edit-dialog' - > - <i class='fa fa-fw fa-bullhorn'></i> - New Post - </a> - </li> - <li> <a href='#' title='Create new page.' - data-toggle='modal' - data-target='#page-edit-dialog' + class='add-post' + data-type='page' > <i class='fa fa-fw fa-bookmark-o'></i> New Page @@ -1618,8 +1717,8 @@ module Guff <a href='#' title='Create new project.' - data-toggle='modal' - data-target='#project-edit-dialog' + class='add-post' + data-type='project' > <i class='fa fa-fw fa-cube'></i> New Project @@ -1633,6 +1732,7 @@ module Guff href='#' class='btn %s' title='Mark as %s.' + data-val='%s' > <i class='fa %s'></i> %s @@ -1679,6 +1779,7 @@ module Guff io << TEMPLATES[:state_button] % [ h(row[:css]), h(row[:name]), + h(row[:id]), h(row[:icon]), h(row[:name]) ] @@ -2368,7 +2469,7 @@ module Guff INSERT INTO states(state_id, state, state_name) VALUES (1, 'draft', 'Draft'), (2, 'posted', 'Posted'), - (3, 'deletd', 'Deleted') + (3, 'deleted', 'Deleted') }, %{ CREATE TABLE posts ( post_id INTEGER PRIMARY KEY, @@ -2395,13 +2496,19 @@ module Guff slug = LOWER(slug) ), - slug_lock BOOLEAN NOT NULL DEFAULT true, + slug_lock BOOLEAN NOT NULL DEFAULT 1, body TEXT NOT NULL DEFAULT '' ) }, %{ CREATE INDEX in_posts_site_id ON posts(site_id) }, %{ + CREATE VIRTUAL TABLE posts_fts USING fts4( + name, + slug, + body + ) + }, %{ CREATE TABLE blogs ( post_id INTEGER PRIMARY KEY REFERENCES posts(post_id) @@ -2540,6 +2647,9 @@ module Guff 'This is the body of a test blog entry.' ) }, %{ + INSERT INTO posts_fts(rowid, name, slug, body) + SELECT post_id, name, slug, body FROM posts + }, %{ INSERT INTO pages(post_id, layout_id) VALUES ( 1, (SELECT layout_id FROM layouts WHERE layout = 'default') diff --git a/src/views/admin-page.ecr b/src/views/admin-page.ecr index 71beb6f..33beb24 100644 --- a/src/views/admin-page.ecr +++ b/src/views/admin-page.ecr @@ -226,7 +226,21 @@ </h4><!-- modal-title --> </div><!-- modal-header --> - <div class='modal-body'> + <div class='modal-body loading-text'> + <p> + <i class='fa fa-spinner fa-spin'></i> + Loading... + </p> + </div><!-- modal-body --> + + <div class='modal-body loading-error'> + <div class='well'> + <i class='fa fa-exclamation-triangle'></i> + Error: <span class='error-text'></span> + </div><!-- well --> + </div><!-- modal-body --> + + <div class='modal-body loading-done'> <div class='form-group'> <label for='user-add-name'> Name @@ -341,7 +355,7 @@ </div><!-- modal-body --> <div class='modal-body loading-error'> - <div class='well well'> + <div class='well'> <i class='fa fa-exclamation-triangle'></i> Error: <span class='error-text'></span> </div><!-- well --> @@ -441,7 +455,7 @@ </h4><!-- modal-title --> </div><!-- modal-header --> - <div class='modal-body'> + <div class='modal-body loading-done'> <div class='row'> <div class='col-md-6'> <div class='form-group'> @@ -542,7 +556,10 @@ State </label> - <div class='btn-group btn-group-justified state-buttons'><%= + <div + id='blog-edit-state' + class='btn-group btn-group-justified state-buttons' + ><%= state_buttons %></div><!-- btn-group --> @@ -604,7 +621,21 @@ </h4><!-- modal-title --> </div><!-- modal-header --> - <div class='modal-body'> + <div class='modal-body loading-text'> + <p> + <i class='fa fa-spinner fa-spin'></i> + Loading... + </p> + </div><!-- modal-body --> + + <div class='modal-body loading-error'> + <div class='well'> + <i class='fa fa-exclamation-triangle'></i> + Error: <span class='error-text'></span> + </div><!-- well --> + </div><!-- modal-body --> + + <div class='modal-body loading-done'> <div class='row'> <div class='col-md-6'> <div class='form-group'> @@ -683,7 +714,10 @@ Layout </label> - <div class='btn-group btn-group-justified state-buttons'> + <div + id='page-edit-layout' + class='btn-group btn-group-justified state-buttons' + > <a href='#' class='btn btn-default' @@ -715,7 +749,10 @@ State </label> - <div class='btn-group btn-group-justified state-buttons'><%= + <div + id='page-edit-state' + class='btn-group btn-group-justified state-buttons' + ><%= state_buttons %></div><!-- btn-group --> @@ -776,7 +813,21 @@ </h4><!-- modal-title --> </div><!-- modal-header --> - <div class='modal-body'> + <div class='modal-body loading-text'> + <p> + <i class='fa fa-spinner fa-spin'></i> + Loading... + </p> + </div><!-- modal-body --> + + <div class='modal-body loading-error'> + <div class='well well'> + <i class='fa fa-exclamation-triangle'></i> + Error: <span class='error-text'></span> + </div><!-- well --> + </div><!-- modal-body --> + + <div class='modal-body loading-done'> <div class='row'> <div class='col-md-6'> <div class='form-group'> @@ -877,7 +928,10 @@ State </label> - <div class='btn-group btn-group-justified state-buttons'><%= + <div + id='project-edit-state' + class='btn-group btn-group-justified state-buttons' + ><%= state_buttons %></div><!-- btn-group --> |