refactoring the content types section (wip)

This commit is contained in:
did 2011-12-19 14:15:11 +01:00
parent 592a110fe5
commit 217000042a
25 changed files with 461 additions and 138 deletions

View File

@ -12,7 +12,9 @@ gem 'cancan', '~> 1.6.7'
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', :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 'kaminari'
gem 'haml', '~> 3.1.3'

View File

@ -9,12 +9,13 @@ GIT
GIT
remote: git://github.com/locomotivecms/custom_fields.git
revision: af21782109d5b4f073949f8f123fb56c6c7227ef
revision: 9cc4a3ca7e2306a59a9ad0c8a837a35d8fc5dfc6
branch: experimental
specs:
custom_fields (1.1.0.rc1)
activesupport (~> 3.1.1)
custom_fields (2.0.0.rc1)
activesupport (~> 3.1.3)
carrierwave-mongoid (~> 0.1.3)
mongoid (~> 2.3.3)
mongoid (~> 2.3.4)
GIT
remote: git://github.com/plataformatec/devise.git

View File

@ -0,0 +1,22 @@
class Locomotive.Models.ContentType extends Backbone.Model
paramRoot: 'content_type'
urlRoot: "#{Locomotive.mount_on}/content_types"
initialize: ->
@_normalize()
_normalize: ->
@set
contents_custom_fields: new Locomotive.Models.CustomFieldsCollection(@get('contents_custom_fields'))
toJSON: ->
_.tap super, (hash) =>
hash.contents_custom_fields = @get('contents_custom_fields').toJSONForSave() if @get('contents_custom_fields')
class Locomotive.Models.ContentTypesCollection extends Backbone.Collection
model: Locomotive.Models.ContentType
url: "#{Locomotive.mount_on}/content_types"

View File

@ -0,0 +1,17 @@
class Locomotive.Models.CustomField extends Backbone.Model
initialize: ->
unless @get('name')?
@set name: @get('label').slugify()
toJSONForSave: ->
_.tap {}, (hash) =>
for key, value of @.toJSON()
hash[key] = value unless _.include(['type_text', 'created_at', 'updated_at'], key)
class Locomotive.Models.CustomFieldsCollection extends Backbone.Collection
model: Locomotive.Models.CustomField
toJSONForSave: ->
@map (model) => model.toJSONForSave()

View File

@ -0,0 +1,34 @@
#= require ../shared/form_view
Locomotive.Views.ContentTypes ||= {}
class Locomotive.Views.ContentTypes.FormView extends Locomotive.Views.Shared.FormView
el: '#content'
events:
'submit': 'save'
initialize: ->
@model = new Locomotive.Models.ContentType(@options.content_type)
window.foo = @model
Backbone.ModelBinding.bind @
render: ->
super()
@render_custom_fields()
return @
render_custom_fields: ->
@custom_fields_view = new Locomotive.Views.ContentTypes.CustomFieldsView model: @model #, errors: @options.errors
@$('#custom_fields_input').append(@custom_fields_view.render().el)
remove: ->
@custom_fields_view.remove()
super

View File

@ -0,0 +1,35 @@
Locomotive.Views.ContentTypes ||= {}
class Locomotive.Views.ContentTypes.CustomFieldEntryView extends Backbone.View
tagName: 'li'
className: 'custom-field'
events:
'click a.edit': 'toggle_form'
'click a.remove': 'remove'
render: ->
$(@el).html(ich.custom_field_entry(@model.toJSON()))
Backbone.ModelBinding.bind @, all: 'class'
return @
toggle_form: (event) ->
event.stopPropagation() & event.preventDefault()
form = @$('ol')
if form.is(':hidden')
form.slideDown()
else
form.slideUp()
remove: (event) ->
event.stopPropagation() & event.preventDefault()
if confirm($(event.target).attr('data-confirm'))
# @$('input[type=text]').editableField('destroy')
super()
@options.parent_view.remove_entry(@model, @)

View File

@ -0,0 +1,95 @@
Locomotive.Views.ContentTypes ||= {}
class Locomotive.Views.ContentTypes.CustomFieldsView extends Backbone.View
tagName: 'div'
className: 'list'
_entry_views = []
events:
'click .new-entry a.add': 'add_entry'
'keypress .new-entry input[type=text]': 'add_on_entry_from_enter'
initialize: ->
_.bindAll(@, 'refresh_position_entries')
render: ->
$(@el).html(ich.custom_fields_list(@model.toJSON()))
@render_entries()
@make_list_sortable()
return @
make_list_sortable: ->
@sortable_list = @$('> ul').sortable
handle: 'span.handle'
items: 'li.custom-field'
axis: 'y'
update: @refresh_position_entries
refresh_position_entries: ->
_.each @_entry_views, (view) ->
view.model.set position: $(view.el).index()
add_entry: (event) ->
event.stopPropagation() & event.preventDefault()
labelInput = @$('.new-entry input[name=label]')
typeInput = @$('.new-entry select')
if labelInput.val() != ''
custom_field = new Locomotive.Models.CustomField label: labelInput.val(), type: typeInput.val()
window.bar = custom_field
@model.get('contents_custom_fields').add(custom_field)
@_insert_entry(custom_field)
@$('.empty').hide()
@sortable_list.sortable('refresh')
labelInput.val('') # reset for further entries
add_on_entry_from_enter: (event) ->
return if event.keyCode != 13
@add_entry(event)
remove_entry: (custom_field, view) ->
@_entry_views = _.reject @_entry_views, (_view) -> _view == view
@model.get('contents_custom_fields').remove(custom_field)
@refresh_position_entries()
@$('.empty').show() if @model.get('contents_custom_fields').length == 0
render_entries: ->
if @model.get('contents_custom_fields').length == 0
@$('.empty').show()
else
_.each @model.get('contents_custom_fields'), (custom_field) =>
@_insert_entry(custom_field)
#
# show_errors: ->
# _.each @options.errors.domain || [], (message) => @show_error(message)
#
# show_error: (message) ->
# _.each (@_entry_views || []), (view) ->
# if new RegExp("^#{view.model.get('name')}").test message
# 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,8 @@
Locomotive.Views.ContentTypes ||= {}
class Locomotive.Views.ContentTypes.NewView extends Locomotive.Views.ContentTypes.FormView
save: (event) ->
@save_in_ajax event,
on_success: (response, xhr) ->
window.location.href = xhr.getResponseHeader('location')

View File

@ -15,8 +15,6 @@ class Locomotive.Views.Sites.NewView extends Locomotive.Views.Shared.FormView
Backbone.ModelBinding.bind @
window.foo = @model
render: ->
super()

View File

@ -228,7 +228,7 @@ form.formtastic {
color: #fff !important;
font-size: 12px;
@include single-text-shadow(#000, 0px, 1px, 1px);
@include single-text-shadow(rgba(0, 0, 0, 0.7), 0px, 1px, 1px);
}
} // div.inline-errors

View File

@ -100,20 +100,25 @@ div#flash-notice {
} // input
div.inline-errors {
background: transparent image-url("locomotive/form/error-arrow.png") no-repeat 17px 0;
margin: 2px 0 0 0;
padding: 8px 0 0 0;
background: transparent image-url("locomotive/form/error-arrow.png") no-repeat 17px 0;
p {
display: inline-block;
width: auto;
line-height: 14px;
margin: 0px;
padding: 3px 12px 4px 30px;
padding: 3px 12px 4px 27px;
@include box-shadow(rgba(0, 0, 0, 0.4) 2px 2px 2px 0);
@include border-radius(3px);
background: #cd0f19 image-url("locomotive/form/icons/error.png") no-repeat 7px center;
color: #fff !important;
text-shadow: #000 0px 1px;
font-size: 11px;
padding-left: 30px;
background: #cd0f19 image-url("locomotive/form/icons/error.png") no-repeat 10px 6px;
font-size: 12px;
@include single-text-shadow(rgba(0, 0, 0, 0.7), 0px, 1px, 1px);
}
} // div.inline-errors
@ -153,6 +158,7 @@ div#flash-notice {
padding: 15px 0;
background: #8b8d9a;
border-top: 1px solid #5F6069;
@include border-bottom-radius(3px);
text-align: right;

View File

@ -3,8 +3,36 @@ module Locomotive
sections 'contents'
respond_to :json, :only => [:create, :update, :destroy]
helper 'Locomotive::CustomFields'
def new
@content_type = current_site.content_types.new
respond_with @content_type
end
def create
@content_type = current_site.content_types.create(params[:content_type])
logger.debug @content_type.contents_custom_fields.inspect
respond_with @content_type, :location => edit_content_type_url(@content_type._id)
end
def edit
@content_type = current_site.content_types.find(params[:id])
respond_with @content_type
end
def update
@content_type = current_site.content_types.find(params[:id])
@content_type.update_attributes(params[:content_type])
respond_with @content_type, :location => edit_content_type_url(@content_type._id)
end
def destroy
destroy! { pages_url }
@content_type = current_site.content_types.find(params[:id])
@content_type.destroy
respond_with @content_type, :location => pages_url
end
end

View File

@ -1,5 +1,23 @@
module Locomotive::ContentTypesHelper
def each_content_type_menu_item(&block)
current_site.content_types.ordered.only(:site_id, :name, :slug).each do |content_type|
next unless content_type.persisted?
item_on = (content_type.slug == @content_type.slug) rescue nil
label = truncate(content_type.name, :length => 15)
url = contents_url(content_type.slug)
css = @content_type && content_type.slug == @content_type.slug ? 'on' : ''
html = submenu_entry(label, url, :i18n => false, :css => css) do
yield(content_type)
end
haml_concat(html)
end
end
# MAX_DISPLAYED_CONTENTS = 4
#
# def fetch_content_types

View File

@ -1,8 +1,9 @@
module Locomotive::CustomFieldsHelper
def options_for_field_kind
%w(string text category boolean date file has_one has_many).map do |kind|
[t("custom_fields.kind.#{kind}"), kind]
def options_for_custom_field_type
# %w(string text category boolean date file has_one has_many).map do |kind|
%w(string text category boolean date file).map do |type|
[t("custom_fields.kind.#{type}"), type]
end
end

View File

@ -1,11 +1,10 @@
module Locomotive
class ContentInstance
include ::Mongoid::Document
include ::Mongoid::Timestamps
include Locomotive::Mongoid::Document
## extensions ##
include ::Mongoid::TargetCustomFields
include ::CustomFields::Target
include Extensions::Shared::Seo
## fields (dynamic fields) ##

View File

@ -4,6 +4,7 @@ module Locomotive
include Locomotive::Mongoid::Document
## extensions ##
include CustomFields::Source
include Extensions::ContentType::ItemTemplate
## fields ##
@ -19,7 +20,7 @@ module Locomotive
## associations ##
belongs_to :site, :class_name => 'Locomotive::Site'
has_many :contents
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? }
@ -35,7 +36,7 @@ module Locomotive
## callbacks ##
before_validation :normalize_slug
before_save :set_default_values
after_destroy :remove_uploaded_files
# after_destroy :remove_uploaded_files
## validations ##
validates_presence_of :site, :name, :slug
@ -47,9 +48,9 @@ module Locomotive
## methods ##
def groupable?
self.group_by_field && group_by_field.category?
end
# def groupable?
# self.group_by_field && group_by_field.category?
# end
def order_manually?
self.order_by == '_position_in_list'
@ -59,70 +60,70 @@ module Locomotive
self.order_direction.blank? || self.order_direction == 'asc'
end
def list_or_group_contents
if self.groupable?
groups = self.contents.klass.send(:"group_by_#{self.group_by_field._alias}", :ordered_contents)
# def list_or_group_contents
# if self.groupable?
# groups = self.contents.klass.send(:"group_by_#{self.group_by_field._alias}", :ordered_contents)
#
# # look for items with no category or unknown ones
# items_without_category = self.contents.find_all { |c| !self.group_by_field.category_ids.include?(c.send(self.group_by_field_name)) }
# if not items_without_category.empty?
# groups << { :name => nil, :items => items_without_category }
# else
# groups
# end
# else
# self.ordered_contents
# end
# end
# look for items with no category or unknown ones
items_without_category = self.contents.find_all { |c| !self.group_by_field.category_ids.include?(c.send(self.group_by_field_name)) }
if not items_without_category.empty?
groups << { :name => nil, :items => items_without_category }
else
groups
end
else
self.ordered_contents
end
end
# def latest_updated_contents
# self.contents.latest_updated.reject { |c| !c.persisted? }
# end
def latest_updated_contents
self.contents.latest_updated.reject { |c| !c.persisted? }
end
# def ordered_contents(conditions = {})
# column = self.order_by.to_sym
#
# list = (if conditions.nil? || conditions.empty?
# self.contents
# else
# conditions_with_names = {}
#
# conditions.each do |key, value|
# # convert alias (key) to name
# field = self.contents_custom_fields.detect { |f| f._alias == key }
#
# case field.kind.to_sym
# when :category
# if (category_item = field.category_items.where(:name => value).first).present?
# conditions_with_names[field._name.to_sym] = category_item._id
# end
# else
# conditions_with_names[field._name.to_sym] = value
# end
# end
#
# self.contents.where(conditions_with_names)
# end).sort { |a, b| (a.send(column) || 0) <=> (b.send(column) || 0) }
#
# return list if self.order_manually?
#
# self.asc_order? ? list : list.reverse
# end
def ordered_contents(conditions = {})
column = self.order_by.to_sym
# 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
list = (if conditions.nil? || conditions.empty?
self.contents
else
conditions_with_names = {}
conditions.each do |key, value|
# convert alias (key) to name
field = self.contents_custom_fields.detect { |f| f._alias == key }
case field.kind.to_sym
when :category
if (category_item = field.category_items.where(:name => value).first).present?
conditions_with_names[field._name.to_sym] = category_item._id
end
else
conditions_with_names[field._name.to_sym] = value
end
end
self.contents.where(conditions_with_names)
end).sort { |a, b| (a.send(column) || 0) <=> (b.send(column) || 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
def group_by_field
@group_by_field ||= self.contents_custom_fields.detect { |f| f._name == self.group_by_field_name }
end
# def highlighted_field
# self.contents_custom_fields.detect { |f| f._name == self.highlighted_field_name }
# end
#
# def group_by_field
# @group_by_field ||= self.contents_custom_fields.detect { |f| f._name == self.group_by_field_name }
# end
protected

View File

@ -1,30 +1,36 @@
- content_for :head do
= include_javascripts :content_types, :custom_fields
= include_stylesheets :fancybox
= render 'locomotive/custom_fields/form', :f => f
- content_for :backbone_view_data do
:plain
{ content_type: #{@content_type.persisted? ? @content_type.to_json : 'null'} }
= f.inputs :name => :information do
= f.input :name, :wrapper_html => { :class => 'highlighted' }
= f.input :slug
= f.input :description
= render 'locomotive/custom_fields/index', :form => f, :collection_name => 'contents'
- unless f.object.new_record?
= f.foldable_inputs :name => :presentation, :class => 'switchable' do
= f.input :highlighted_field_name, :as => :select, :collection => options_for_highlighted_field(f.object, 'contents'), :include_blank => false
= f.input :group_by_field_name, :as => :select, :collection => options_for_group_by_field(f.object, 'contents')
= f.custom_input :item_template, :css => 'small-code' do
%code{ :class => 'html' }
= f.text_area :raw_item_template, :class => 'small'
= f.foldable_inputs :name => :options, :class => 'switchable' do
= f.input :order_by, :as => :select, :collection => options_for_order_by(f.object, 'contents'), :include_blank => false
= f.input :order_direction, :as => :select, :collection => options_for_order_direction, :include_blank => false, :wrapper_html => { :style => "#{'display: none' if f.object.order_manually?}" }
= f.custom_input :api_enabled, :css => 'toggle' do
= f.check_box :api_enabled
= hidden_field_tag 'content_type[api_accounts][]', ''
= f.input :api_accounts, :as => :select, :collection => current_site.accounts.collect { |a| ["#{a.name} <#{a.email}>", a.id] }, :include_blank => false, :multiple => true, :wrapper_html => { :class => 'multiple', :style => (f.object.api_enabled? ? '' : 'display: none') }
= f.inputs :name => :custom_fields, :class => "inputs foldable #{'folded' if inputs_folded?(@content_type)}" do
= f.input :contents_custom_fields, :as => :'Locomotive::Empty', :label => false, :wrapper_html => { :id => 'custom_fields_input' }
/ = render 'locomotive/custom_fields/index', :form => f, :collection_name => 'contents'
/
/ - unless f.object.new_record?
/ = f.foldable_inputs :name => :presentation, :class => 'switchable' do
/ = f.input :highlighted_field_name, :as => :select, :collection => options_for_highlighted_field(f.object, 'contents'), :include_blank => false
/ = f.input :group_by_field_name, :as => :select, :collection => options_for_group_by_field(f.object, 'contents')
/ = f.custom_input :item_template, :css => 'small-code' do
/ %code{ :class => 'html' }
/ = f.text_area :raw_item_template, :class => 'small'
/
/ = f.foldable_inputs :name => :options, :class => 'switchable' do
/ = f.input :order_by, :as => :select, :collection => options_for_order_by(f.object, 'contents'), :include_blank => false
/ = f.input :order_direction, :as => :select, :collection => options_for_order_direction, :include_blank => false, :wrapper_html => { :style => "#{'display: none' if f.object.order_manually?}" }
/ = f.custom_input :api_enabled, :css => 'toggle' do
/ = f.check_box :api_enabled
/ = hidden_field_tag 'content_type[api_accounts][]', ''
/ = f.input :api_accounts, :as => :select, :collection => current_site.accounts.collect { |a| ["#{a.name} <#{a.email}>", a.id] }, :include_blank => false, :multiple => true, :wrapper_html => { :class => 'multiple', :style => (f.object.api_enabled? ? '' : 'display: none') }
/
/
/
/

View File

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

View File

@ -0,0 +1,45 @@
%script{ :type => 'text/html', :id => 'custom_fields_list' }
%p.empty{ :style => 'display: none' }!= t('.empty')
%ul
%hr
.new-entry
= text_field_tag 'label', '', :id => '', :placeholder => t('.default_label')
= select_tag 'type', options_for_select(options_for_custom_field_type)
%span.actions
= link_to t('locomotive.buttons.new_item'), '#', :class => 'add'
%script{ :type => 'text/html', :id => 'custom_field_entry' }
= f.semantic_fields_for :contents_custom_field, CustomFields::Field.new do |g|
%span.handle
= image_tag 'locomotive/form/icons/drag.png'
%span.label
= g.text_field :label, :class => 'label'
%span.type
= g.select :type, options_for_custom_field_type, {}, { :class => 'type' }
%span.required
= g.check_box :required, :class => 'required'
= g.label :required, t('.is_required')
%ol.nested{ :style => 'display: none' }
= g.input :name, :input_html => { :class => 'name' }
= g.input :hint, :input_html => { :class => 'hint' }
%span.actions
= link_to 'edit', '#', :class => 'edit'
= link_to 'x', '#', :class => 'remove', :confirm => t('locomotive.messages.confirm')

View File

@ -18,17 +18,17 @@
- 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')
%ul
- content_type.latest_updated_contents.each do |content|
%li
= link_to truncate(content.send(content_type.highlighted_field_name).to_s, :length => 20), edit_admin_content_path(content_type.slug, content)
%span= time_ago_in_words(content.updated_at)
/ .inner
/ %h2!= t('locomotive.contents.index.lastest_items')
/ %ul
/ - content_type.latest_updated_contents.each do |content|
/ %li
/ = link_to truncate(content.send(content_type.highlighted_field_name).to_s, :length => 20), edit_admin_content_path(content_type.slug, content)
/ %span= time_ago_in_words(content.updated_at)
- other_content_types do |content_types|
.inner
%ul.big-links
- content_types.each do |content_type|
%li
= link_to truncate(content_type.name, :length => 20), contents_url(content_type.slug)
/ - other_content_types do |content_types|
/ .inner
/ %ul.big-links
/ - content_types.each do |content_type|
/ %li
/ = link_to truncate(content_type.name, :length => 20), contents_url(content_type.slug)

View File

@ -74,7 +74,7 @@ en:
delete_file: Delete file
has_many:
empty: Empty
index:
form:
is_required: is required
default_label: Field name

View File

@ -73,7 +73,7 @@ fr:
delete_file: Supprimer fichier
has_many:
empty: Vide
index:
form:
is_required: est obligatoire
default_label: Nom du champ

View File

@ -33,11 +33,18 @@ x edit my site
x export
x site picker
- content types
- change in main menu
x move content instances into their own collection
- manage custom_fields
- list
- crud
- move content instances into their own collection
x automatic name
x required
x editable plugin: add class depending on the type => surrounding span instead
x position
x open the nested form
x remove an entry
- look n feel
- show / hide options of a field based on its type
- display errors
- change in main menu
- manage contents
- list
- crud

View File

@ -20,15 +20,15 @@ module CustomFields
end
end
module Category
class Item
def to_liquid
{ 'id' => self._id.to_s, 'name' => self.name }
end
end
end
# module Category
# class Item
#
# def to_liquid
# { 'id' => self._id.to_s, 'name' => self.name }
# end
#
# end
# end
end
end

View File

@ -9,7 +9,7 @@ module Locomotive
included do
include ::Mongoid::Document
include ::Mongoid::Timestamps
include ::Mongoid::CustomFields
# include ::Mongoid::CustomFields
end
def as_json(options={})