minimal version of the new UI for content types

This commit is contained in:
did 2011-12-21 15:24:39 +01:00
commit 00283993c0
29 changed files with 335 additions and 109 deletions

View File

@ -13,8 +13,8 @@ gem 'bson_ext', '~> 1.4.0'
gem 'mongoid', '~> 2.3.3'
gem 'locomotive_mongoid_acts_as_tree', :git => 'git@github.com:locomotivecms/mongoid_acts_as_tree.git'
# gem 'custom_fields', :git => 'git://github.com/locomotivecms/custom_fields.git'
# gem 'custom_fields', :path => '../gems/custom_fields' # DEV
gem 'custom_fields', :git => 'git://github.com/locomotivecms/custom_fields.git', :branch => 'experimental'
gem 'custom_fields', :path => '../gems/custom_fields' # DEV
# gem 'custom_fields', :git => 'git://github.com/locomotivecms/custom_fields.git', :branch => 'experimental'
gem 'kaminari'
gem 'haml', '~> 3.1.3'

View File

@ -7,16 +7,6 @@ GIT
fssm (>= 0.2.7)
sass (~> 3.1)
GIT
remote: git://github.com/locomotivecms/custom_fields.git
revision: 9cc4a3ca7e2306a59a9ad0c8a837a35d8fc5dfc6
branch: experimental
specs:
custom_fields (2.0.0.rc1)
activesupport (~> 3.1.3)
carrierwave-mongoid (~> 0.1.3)
mongoid (~> 2.3.4)
GIT
remote: git://github.com/plataformatec/devise.git
revision: ede004169c6af7416f8c4e3fc29a653bee133f60
@ -38,6 +28,14 @@ GIT
specs:
locomotive_mongoid_acts_as_tree (0.1.5.7)
PATH
remote: ../gems/custom_fields
specs:
custom_fields (2.0.0.rc1)
activesupport (~> 3.1.3)
carrierwave-mongoid (~> 0.1.3)
mongoid (~> 2.3.4)
GEM
remote: http://rubygems.org/
specs:

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

View File

@ -13,7 +13,8 @@ class Locomotive.Models.ContentType extends Backbone.Model
toJSON: ->
_.tap super, (hash) =>
hash.contents_custom_fields = @get('contents_custom_fields').toJSONForSave() if @get('contents_custom_fields')
delete hash.contents_custom_fields
hash.contents_custom_fields_attributes = @get('contents_custom_fields').toJSONForSave() if @get('contents_custom_fields')
class Locomotive.Models.ContentTypesCollection extends Backbone.Collection

View File

@ -21,13 +21,25 @@ class Locomotive.Views.ContentTypes.FormView extends Locomotive.Views.Shared.For
@render_custom_fields()
@slugify_name() # slugify the slug field from name
return @
render_custom_fields: ->
@custom_fields_view = new Locomotive.Views.ContentTypes.CustomFieldsView model: @model #, errors: @options.errors
@custom_fields_view = new Locomotive.Views.ContentTypes.CustomFieldsView model: @model
@$('#custom_fields_input').append(@custom_fields_view.render().el)
slugify_name: ->
@$('#content_type_name').slugify(target: @$('#content_type_slug'), sep: '_')
show_error: (attribute, message, html) ->
if attribute == 'contents_custom_fields'
for _message, index in message
@custom_fields_view._entry_views[index].show_error(_message[0])
else
super
remove: ->
@custom_fields_view.remove()
super

View File

@ -7,7 +7,7 @@ class Locomotive.Views.ContentTypes.CustomFieldEntryView extends Backbone.View
className: 'custom-field'
events:
'click a.edit': 'toggle_form'
'click a.toggle': 'toggle'
'click a.remove': 'remove'
render: ->
@ -15,9 +15,11 @@ class Locomotive.Views.ContentTypes.CustomFieldEntryView extends Backbone.View
Backbone.ModelBinding.bind @, all: 'class'
@$('span.label-input input[type=text], span.type-input select').editableField()
return @
toggle_form: (event) ->
toggle: (event) ->
event.stopPropagation() & event.preventDefault()
form = @$('ol')
@ -26,10 +28,14 @@ class Locomotive.Views.ContentTypes.CustomFieldEntryView extends Backbone.View
else
form.slideUp()
show_error: (message) ->
html = $("<span class=\"inline-errors\">#{message}</div>")
@$('span.required-input').after(html)
remove: (event) ->
event.stopPropagation() & event.preventDefault()
if confirm($(event.target).attr('data-confirm'))
# @$('input[type=text]').editableField('destroy')
@$('span.label-input input[type=text], span.type-input select').editableField('destroy')
super()
@options.parent_view.remove_entry(@model, @)

View File

@ -72,9 +72,18 @@ class Locomotive.Views.ContentTypes.CustomFieldsView extends Backbone.View
if @model.get('contents_custom_fields').length == 0
@$('.empty').show()
else
_.each @model.get('contents_custom_fields'), (custom_field) =>
@model.get('contents_custom_fields').each (custom_field) =>
@_insert_entry(custom_field)
_insert_entry: (custom_field) ->
view = new Locomotive.Views.ContentTypes.CustomFieldEntryView model: custom_field, parent_view: @
(@_entry_views ||= []).push(view)
@$('ul').append(view.render().el)
@refresh_position_entries()
#
# show_errors: ->
# _.each @options.errors.domain || [], (message) => @show_error(message)
@ -85,11 +94,4 @@ class Locomotive.Views.ContentTypes.CustomFieldsView extends Backbone.View
# html = $('<span></span>').addClass('inline-errors').html(message)
# view.$('input[type=text].path').after(html)
_insert_entry: (custom_field) ->
view = new Locomotive.Views.ContentTypes.CustomFieldEntryView model: custom_field, parent_view: @
(@_entry_views ||= []).push(view)
@$('ul').append(view.render().el)
@refresh_position_entries()

View File

@ -0,0 +1,6 @@
Locomotive.Views.ContentTypes ||= {}
class Locomotive.Views.ContentTypes.EditView extends Locomotive.Views.ContentTypes.FormView
save: (event) ->
@save_in_ajax event

View File

@ -75,8 +75,11 @@ class Locomotive.Views.Shared.FormView extends Backbone.View
show_errors: (errors) ->
for attribute, message of errors
html = $("<div class=\"inline-errors\"><p>#{message[0]}</p></div>")
@show_error attribute, message[0], html
if _.isArray(message[0])
@show_error attribute, message
else
html = $("<div class=\"inline-errors\"><p>#{message[0]}</p></div>")
@show_error attribute, message[0], html
show_error: (attribute, message, html) ->
input = @$("##{@model.paramRoot}_#{attribute}")

View File

@ -60,13 +60,17 @@ form.formtastic {
.list {
.empty {
text-align: left;
color: #BDBFC9;
text-shadow: #fff 1px 1px 1px;
font-weight: bold;
font-size: 13px;
}
li, .entry, .new-entry {
position: relative;
margin-bottom: 10px;
height: 30px;
line-height: 30px;
height: 31px;
line-height: 31px;
padding: 0 10px;
@ -118,7 +122,7 @@ form.formtastic {
padding-left: 10px;
}
a.remove {
a.remove, a.toggle {
display: inline-block;
width: 16px;
height: 16px;
@ -127,13 +131,20 @@ form.formtastic {
top: 4px;
outline: none;
background: transparent image-url("locomotive/list/icons/trash_off.png") repeat 0 0;
text-indent: -9999px;
&:hover {
background-image: image-url("locomotive/list/icons/trash.png");
&.remove {
background: transparent image-url("locomotive/list/icons/trash_off.png") repeat 0 0;
&:hover {
background-image: image-url("locomotive/list/icons/trash.png");
}
}
&.toggle {
background: transparent image-url("locomotive/list/icons/toggle_off.png") repeat 0 0;
&:hover {
background-image: image-url("locomotive/list/icons/toggle.png");
}
}
}
}
@ -382,7 +393,7 @@ form.formtastic {
}
&.editable {
background-image: image-url("locomotive//icons/membership_edit.png");
background-image: image-url("locomotive/icons/membership_edit.png");
background-position: 0px 3px;
}
}
@ -427,8 +438,119 @@ form.formtastic {
}
} // > li#account_sites_input
&#custom_fields_input {
padding-top: 10px;
li.custom-field {
height: auto;
.editable {
display: inline-block;
line-height: 16px;
}
span.handle {
position: relative;
top: 3px;
margin-left: 5px;
}
span.label-input {
input[type=text] {
float: none;
display: inline;
@include default-input-style;
margin-left: 5px;
padding: 2px 3px;
font-size: 12px;
}
.editable {
width: 160px;
}
}
span.type-input {
display: inline-block;
width: 150px;
.editable {
// font-style: italic;
color: #585A69;
font-weight: normal;
}
}
span.required-input {
margin-left: 10px;
label {
float: none;
width: auto;
font-weight: normal;
}
}
} // li.custom-field
.new-entry {
input[type=text] {
@include default-input-style;
margin-left: 5px;
padding: 2px 3px;
font-size: 12px;
}
} // .new-entry
} // > li#custom_fields_input
} // > li
&.nested {
width: 858px;
border: none;
@include border-radius(0);
@include box-shadow(transparent 0 0 0 0);
background: transparent;
> li.input {
height: auto;
padding-bottom: 5px;
padding-left: 29px;
background: transparent;
border-top: 1px solid #CCC;
@include border-radius(0);
@include box-shadow(transparent 0 0 0 0);
label.label {
width: 174px;
padding-top: 0px;
line-height: 28px;
}
&.string, &.password {
input[type=text], input[type=password] {
width: 623px;
}
p.inline-hints {
margin-top: 0px;
}
} // li.string, li.password
p.inline-hints {
margin-left: 174px;
}
} // li.input
} // ol.nested
} //ol
} // fieldset

View File

@ -14,10 +14,8 @@ module Locomotive
field :_visible, :type => Boolean, :default => true
## validations ##
validate :require_highlighted_field
# validate :validate_uniqueness_of_slug
validates_uniqueness_of :_slug, :scope => [:content_type_id]
validates_presence_of :_slug
validate :require_highlighted_field
validates :_slug, :presence => true, :uniqueness => { :scope => :content_type_id }
## associations ##
belongs_to :site
@ -82,9 +80,29 @@ module Locomotive
protected
# Sets the slug of the instance by using the value of the highlighted field
# (if available). If a sibling content instance has the same permalink then a
# unique one will be generated
def set_slug
self._slug = self.highlighted_field_value.dup if self._slug.blank? && self.highlighted_field_value.present?
self._slug.permalink! if self._slug.present?
self._slug = highlighted_field_value.dup if _slug.blank? && highlighted_field_value.present?
if _slug.present?
self._slug.permalink!
self._slug = next_unique_slug if slug_already_taken?
end
end
# Return the next available unique slug as a string
def next_unique_slug
slug = _slug.gsub(/-\d*$/, '')
last_slug = _parent.contents.where(:_id.ne => _id, :_slug => /^#{slug}-?\d*?$/i).order_by(:_slug).last._slug
next_number = last_slug.scan(/-(\d)$/).flatten.first.to_i + 1
[slug, next_number].join('-')
end
def slug_already_taken?
_parent.contents.where(:_id.ne => _id, :_slug => _slug).any?
end
def set_visibility
@ -109,12 +127,6 @@ module Locomotive
end
end
# def validate_uniqueness_of_slug
# if self._parent.contents.any? { |c| c._id != self._id && c._slug == self._slug }
# self.errors.add(:_slug, :taken)
# end
# end
def highlighted_field_alias
self.content_type.highlighted_field._alias.to_sym
end

View File

@ -20,12 +20,7 @@ module Locomotive
## associations ##
belongs_to :site, :class_name => 'Locomotive::Site'
has_many :contents, :class_name => 'Locomotive::ContentInstance'
# embeds_many :contents, :class_name => 'Locomotive::ContentInstance', :validate => false do
# def visible
# @target.find_all { |c| c.visible? }
# end
# end
has_many :contents, :class_name => 'Locomotive::ContentInstance', :dependent => :destroy
## named scopes ##
scope :ordered, :order_by => :updated_at.desc
@ -35,6 +30,7 @@ module Locomotive
## callbacks ##
before_validation :normalize_slug
after_validation :bubble_fields_errors_up
before_save :set_default_values
# after_destroy :remove_uploaded_files
@ -60,6 +56,14 @@ module Locomotive
self.order_direction.blank? || self.order_direction == 'asc'
end
def to_presenter
Locomotive::ContentTypePresenter.new(self)
end
def as_json(options = {})
self.to_presenter.as_json
end
# def list_or_group_contents
# if self.groupable?
# groups = self.contents.klass.send(:"group_by_#{self.group_by_field._alias}", :ordered_contents)
@ -75,11 +79,11 @@ module Locomotive
# self.ordered_contents
# end
# end
#
# def latest_updated_contents
# self.contents.latest_updated.reject { |c| !c.persisted? }
# end
#
# def ordered_contents(conditions = {})
# column = self.order_by.to_sym
#
@ -103,20 +107,20 @@ module Locomotive
# end
#
# self.contents.where(conditions_with_names)
# end).sort { |a, b| (a.send(column) || 0) <=> (b.send(column) || 0) }
# end).sort { |a, b| (a.send(column) && b.send(column)) ? (a.send(column) || 0) <=> (b.send(column) || 0) : 0 }
#
# return list if self.order_manually?
#
# self.asc_order? ? list : list.reverse
# end
#
# def sort_contents!(ids)
# ids.each_with_index do |id, position|
# self.contents.find(BSON::ObjectId(id))._position_in_list = position
# end
# self.save
# end
#
# def highlighted_field
# self.contents_custom_fields.detect { |f| f._name == self.highlighted_field_name }
# end
@ -129,7 +133,7 @@ module Locomotive
def set_default_values
self.order_by ||= 'created_at'
self.highlighted_field_name ||= self.contents_custom_fields.first._name
self.highlighted_field_name ||= self.contents_custom_fields.first.name
end
def normalize_slug
@ -137,13 +141,16 @@ module Locomotive
self.slug.permalink! if self.slug.present?
end
# def remove_uploaded_files # callbacks are not called on each content so we do it manually
# self.contents.each do |content|
# self.contents_custom_fields.each do |field|
# content.send(:"remove_#{field._name}!") if field.kind == 'file'
# end
# end
# end
def bubble_fields_errors_up
return if self.errors[:contents_custom_fields].empty?
self.errors.set(:contents_custom_fields, [])
self.contents_custom_fields.each do |field|
next if field.valid?
self.errors.add(:contents_custom_fields, field.errors.to_a)
end
end
end
end

View File

@ -0,0 +1,15 @@
module Locomotive
class ContentTypePresenter < BasePresenter
delegate :name, :description, :slug, :order_by, :order_direction, :highlighted_field_name, :group_by_field_name, :api_accounts, :to => :source
def contents_custom_fields
self.source.ordered_contents_custom_fields.collect(&:as_json)
end
def included_methods
super + %w(name description slug order_by order_direction highlighted_field_name group_by_field_name api_accounts contents_custom_fields)
end
end
end

View File

@ -17,5 +17,3 @@
= render 'form', :f => f
= render 'locomotive/shared/form_actions', :back_url => contents_url(@content_type.slug_was), :button_label => :update
= render 'locomotive/custom_fields/edit_field', :content_type => @content_type

View File

@ -12,6 +12,4 @@
= render 'form', :f => f
= render 'locomotive/shared/form_actions', :back_url => pages_url, :button_label => :create
/ = render 'locomotive/custom_fields/edit_field', :content_type => @content_type
= render 'locomotive/shared/form_actions', :back_url => pages_url, :button_label => :create

View File

@ -1,7 +1,7 @@
- if breadcrumb_root
- title t('.title.breadcrumb', :root => link_to(breadcrumb_root._label, breadcrumb_url), :type => @content_type.name.capitalize)
- else
- title t('.title.default', :type => @content_type.name.capitalize)
/ - if breadcrumb_root
/ - title t('.title.breadcrumb', :root => link_to(breadcrumb_root._label, breadcrumb_url), :type => @content_type.name.capitalize)
/ - else
- title t('.title.default', :type => @content_type.name.capitalize)
- content_for :submenu do
= render 'locomotive/shared/menu/contents'

View File

@ -23,13 +23,13 @@
%span.handle
= image_tag 'locomotive/form/icons/drag.png'
%span.label
%span.label-input
= g.text_field :label, :class => 'label'
%span.type
= g.select :type, options_for_custom_field_type, {}, { :class => 'type' }
%span.type-input
= g.select :type, options_for_custom_field_type, {}, { :class => 'type' }
%span.required
%span.required-input
= g.check_box :required, :class => 'required'
= g.label :required, t('.is_required')
@ -41,5 +41,5 @@
= g.input :hint, :input_html => { :class => 'hint' }
%span.actions
= link_to 'edit', '#', :class => 'edit'
= link_to 'toggle', '#', :class => 'toggle'
= link_to 'x', '#', :class => 'remove', :confirm => t('locomotive.messages.confirm')

View File

@ -12,14 +12,15 @@
%span= time_ago_in_words(page.updated_at)
- each_content_type_menu_item do |content_type|
.header
%p= link_to t('locomotive.contents.index.new'), new_content_url(content_type.slug)
.wrapper
.header
%p= link_to t('locomotive.contents.index.new'), new_content_url(content_type.slug)
- if can? :manage, content_type
%p.edit= link_to t('locomotive.contents.index.edit'), edit_content_type_url(content_type)
- if can? :manage, content_type
%p.edit= link_to t('locomotive.contents.index.edit'), edit_content_type_url(content_type)
/ .inner
/ %h2!= t('locomotive.contents.index.lastest_items')
.inner
%h2!= t('locomotive.contents.index.lastest_items')
/ %ul
/ - content_type.latest_updated_contents.each do |content|
/ %li

View File

@ -5,4 +5,5 @@
#
require 'formtastic'
Formtastic::FormBuilder.i18n_lookups_by_default = true
Formtastic::FormBuilder.configure :escape_html_entities_in_hints_and_labels, false

View File

@ -80,10 +80,6 @@ en:
source: "The current file is available here %{url}"
update:
source: "The current file is available here %{url}"
custom_fields:
field:
_alias: "Property available in liquid templates"
hint: "Text displayed in the model form just below the field"
content_instance:
_slug: "Property used to generate the url of a page working as a template for this content type (ex: \"template_page/{{ your_object._permalink }})\"."
seo_title: "The value you fill in will replace the SEO title of the templatized page related to your model."
@ -94,7 +90,12 @@ en:
samples: "If enabled, the import process will also copy contents and assets"
reset: "If enabled, all the data of your site will be destroyed before importing the new site"
content_type:
name: "We suggest you to type the plural form of the model. Ex: Projects, Recipes, Posts, Articles, ...etc"
slug: "It will be used as the name of the collection in the liquid templates. Ex: {{ contents.my_projects }}"
item_template: "You can customize the text displayed for each item in the list. Simply use Liquid. Ex: {{ content.name }})"
api_enabled: "It is used to let people from outside to create new instances (example: messages in a contact form)"
api_accounts: "A notification email will be sent to each of the accounts listed above when a new instance is created"
"custom_fields/field":
name: "Name of the property for liquid templates. Ex: &#123;&#123; your_object.&lt;name_of_your_field&gt; &#125;&#125;"
hint: "Text displayed in the model form just below the field"

View File

@ -41,9 +41,12 @@ x edit my site
x position
x open the nested form
x remove an entry
- look n feel
x look n feel
x display errors
x slugify
- other content type options
- show / hide options of a field based on its type
- display errors
- refactor highlighted_field (field id instead of name)
- change in main menu
- manage contents
- list

View File

@ -305,7 +305,7 @@ module Locomotive
content.send(field.safe_alias.to_sym)
end)
hash[field._alias] = value unless value.blank?
hash[field._alias] = value if value.present? || value == false # False values should be preserved in the export
end
data << { content.highlighted_field_value => hash }

View File

@ -9,13 +9,6 @@ module Locomotive
included do
include ::Mongoid::Document
include ::Mongoid::Timestamps
# include ::Mongoid::CustomFields
end
def as_json(options={})
attrs = super(options)
attrs["id"] = attrs["_id"]
attrs
end
end

View File

@ -2,6 +2,16 @@
require 'mongoid'
module Mongoid
module Document
def as_json(options={})
attrs = super(options)
attrs["id"] = attrs["_id"]
attrs
end
end
end
# Limit feature for embedded documents
module Mongoid #:nodoc:

View File

@ -9,7 +9,7 @@ gemfile = File.expand_path('../../../../Gemfile', __FILE__)
# delayed job version is released?
#
require 'yaml'
YAML::ENGINE.yamler = 'syck'
YAML::ENGINE.yamler = 'syck' if defined?(YAML::ENGINE)
if File.exist?(gemfile)
ENV['BUNDLE_GEMFILE'] = gemfile

View File

@ -25,7 +25,8 @@ describe Locomotive::Export do
end
it 'deals with real booleans' do
@project_data.first.values.first['active'].should be_true
@project_data.first.values.first['active'].should === true
@project_data.last.values.first['active'].should === false
end
it 'stores the list of highlighted values in a has_many relationship' do

View File

@ -14,7 +14,6 @@ describe Locomotive::ContentInstance do
end
describe '#validation' do
it 'is valid' do
build_content.should be_valid
end
@ -32,14 +31,37 @@ describe Locomotive::ContentInstance do
content.should_not be_valid
content.errors[:_slug].should == ["can't be blank"]
end
end
it 'has an unique permalink' do
build_content.save; @content_type = Locomotive::ContentType.find(@content_type._id)
content = build_content
content.should_not be_valid
content.errors[:_slug].should == ["is already taken"]
context 'setting the slug' do
before :each do
build_content(:_slug => 'dogs').tap(&:save!)._slug.should == 'dogs'
end
it 'uses the given slug if it is unique' do
build_content(:_slug => 'monkeys').tap(&:save!)._slug.should == 'monkeys'
build_content(:_slug => 'cats-2').tap(&:save!)._slug.should == 'cats-2'
end
it 'appends a number to the end of the slug if it is not unique' do
build_content(:_slug => 'dogs').tap(&:save!)._slug.should == 'dogs-1'
build_content(:_slug => 'dogs').tap(&:save!)._slug.should == 'dogs-2'
build_content(:_slug => 'dogs-2').tap(&:save!)._slug.should == 'dogs-3'
build_content(:_slug => 'dogs-2').tap(&:save!)._slug.should == 'dogs-4'
end
it 'ignores the case of a slug' do
build_content(:_slug => 'dogs').tap(&:save!)._slug.should == 'dogs-1'
build_content(:_slug => 'DOGS').tap(&:save!)._slug.should == 'dogs-2'
end
it 'correctly handles slugs with multiple numbers' do
build_content(:_slug => 'fish-1-2').tap(&:save!)._slug.should == 'fish-1-2'
build_content(:_slug => 'fish-1-2').tap(&:save!)._slug.should == 'fish-1-3'
build_content(:_slug => 'fish-1-hi').tap(&:save!)._slug.should == 'fish-1-hi'
build_content(:_slug => 'fish-1-hi').tap(&:save!)._slug.should == 'fish-1-hi-1'
end
end
describe "#navigation" do

View File

@ -89,6 +89,20 @@ describe Locomotive::ContentType do
@content_type.order_direction = 'desc'
@content_type.ordered_contents.collect(&:name).should == %w(Sacha Did)
end
it 'returns a list of contents ordered by a Date column when first instance is missing the value' do
@content_type = FactoryGirl.build(:content_type, :order_by => 'created_at')
@content_type.content_custom_fields.build :label => 'Active at', :name => 'active_at', :kind => 'Date'
e = Date.parse('01/01/2001')
l = Date.parse('02/02/2002')
[nil,e,l].each { |d| @content_type.contents << @content_type.content_klass.new(:active_at => d) }
@content_type.order_by = 'active_at'
@content_type.order_direction = 'asc'
lambda { @content_type.ordered_contents }.should_not raise_error(ArgumentError)
@content_type.ordered_contents.map(&:active_at).should == [nil,e,l]
@content_type.order_direction = 'desc'
@content_type.ordered_contents.map(&:active_at).should == [l,e,nil]
end
end