diff --git a/Gemfile b/Gemfile index d964ebdb..dd4e7e60 100644 --- a/Gemfile +++ b/Gemfile @@ -5,9 +5,9 @@ source "http://gems.github.com" gem "rails", "3.0.0.beta3" gem "liquid" -gem "bson_ext", '0.20.1' +gem "bson_ext", ">= 1.0.1" gem "mongo_ext" -gem "mongoid", ">= 2.0.0.beta4" +gem "mongoid", ">= 2.0.0.beta6" gem "mongoid_acts_as_tree", :git => 'git://github.com/evansagge/mongoid_acts_as_tree.git' gem "warden" gem "devise", ">= 1.1.rc0" diff --git a/app/helpers/admin/custom_fields_helper.rb b/app/helpers/admin/custom_fields_helper.rb new file mode 100644 index 00000000..5c27646e --- /dev/null +++ b/app/helpers/admin/custom_fields_helper.rb @@ -0,0 +1,18 @@ +module Admin::CustomFieldsHelper + + # def options_for_field_kind(selected = nil) + # # %w{String Text Boolean Email File Date} + # options = %w{String Text}.map do |kind| + # [t("admin.custom_fields.kind.#{kind.downcase}"), kind] + # end + # options_for_select(options, selected) + # end + + def options_for_field_kind(selected = nil) + # %w{String Text Boolean Email File Date} + options = %w{String Text}.map do |kind| + [t("admin.custom_fields.kind.#{kind.downcase}"), kind] + end + end + +end \ No newline at end of file diff --git a/app/models/asset_collection.rb b/app/models/asset_collection.rb index ed0e264e..182f093f 100644 --- a/app/models/asset_collection.rb +++ b/app/models/asset_collection.rb @@ -15,7 +15,7 @@ class AssetCollection embeds_many :asset_fields # FIXME (custom fields) ## behaviours ## - accepts_nested_attributes_for :asset_fields # FIXME (custom fields) + accepts_nested_attributes_for :asset_fields, :allow_destroy => true # FIXME (custom fields) ## callbacks ## before_validate :normalize_slug @@ -38,6 +38,10 @@ class AssetCollection def assets_order=(order) @assets_order = order end + + def ordered_asset_fields # FIXME (custom fields) + self.asset_fields.sort { |a, b| (a.position || 0) <=> (b.position || 0) } + end protected diff --git a/app/models/asset_field.rb b/app/models/asset_field.rb index 010ad4d5..8b2a8db9 100644 --- a/app/models/asset_field.rb +++ b/app/models/asset_field.rb @@ -12,6 +12,8 @@ class AssetField ## validations ## validates_presence_of :label, :kind + embedded_in :asset_collection, :inverse_of => :asset_fields + ## methods ## def field_type @@ -49,7 +51,7 @@ class AssetField end def increment_counter! - next_value = self._parent.send(:"#{self.association_name}_counter") + 1 + next_value = (self._parent.send(:"#{self.association_name}_counter") || 0) + 1 self._parent.send(:"#{self.association_name}_counter=", next_value) next_value end diff --git a/app/views/admin/asset_collections/_custom_fields.html.haml b/app/views/admin/asset_collections/_custom_fields.html.haml new file mode 100644 index 00000000..81aa4038 --- /dev/null +++ b/app/views/admin/asset_collections/_custom_fields.html.haml @@ -0,0 +1,43 @@ += f.foldable_inputs :name => :custom_fields, :class => 'editable-list fields off' do + - f.object.ordered_asset_fields.each do |field| + = f.fields_for :asset_fields, field, :child_index => field._index do |g| + %li{ :class => "item added #{'error' unless field.errors.empty?}"} + %span.handle + = image_tag 'admin/form/icons/drag.png' + + = g.hidden_field :position, :class => 'position' + + = g.text_field :label + + — + + %em= t("admin.custom_fields.kind.#{field.kind.downcase}") + + = g.select :kind, options_for_field_kind + +   + + %span.actions + = link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm') + + = f.fields_for :asset_fields, @collection.asset_fields.build(:label => 'field name'), :child_index => '-1' do |g| + %li{ :class => 'item template' } + %span.handle + = image_tag 'admin/form/icons/drag.png' + + = g.hidden_field :position, :class => 'position' + + = g.text_field :label, :class => 'string label void' + + — + + %em + + = g.select :kind, options_for_field_kind + +   + + %span.actions + = link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm') + %button{ :class => 'button light add', :type => 'button' } + %span= t('admin.buttons.new_item') \ No newline at end of file diff --git a/app/views/admin/asset_collections/edit.html.haml b/app/views/admin/asset_collections/edit.html.haml index d0fe0adc..032ffb98 100644 --- a/app/views/admin/asset_collections/edit.html.haml +++ b/app/views/admin/asset_collections/edit.html.haml @@ -1,7 +1,7 @@ - title link_to(@collection.name.blank? ? @collection.name_was : @collection.name, '#', :rel => 'asset_collection_name', :title => t('.ask_for_name'), :class => 'editable') - content_for :head do - = javascript_include_tag 'admin/asset_collections.js' + = javascript_include_tag 'admin/asset_collections.js', 'admin/custom_fields' - content_for :submenu do = render 'admin/shared/menu/assets' @@ -25,4 +25,6 @@ = f.input :name = f.input :slug, :required => false + = render 'custom_fields', :f => f + = render 'admin/shared/form_actions', :delete_button => link_to(content_tag(:span, t('.destroy')), admin_asset_collection_url(@collection), :confirm => t('admin.messages.confirm'), :method => :delete, :class => 'button small remove'), :button_label => :update \ No newline at end of file diff --git a/app/views/admin/current_sites/_form.html.haml b/app/views/admin/current_sites/_form.html.haml index cc1612f0..863888d9 100644 --- a/app/views/admin/current_sites/_form.html.haml +++ b/app/views/admin/current_sites/_form.html.haml @@ -24,7 +24,7 @@ %span.actions = link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm') - %li.item.new + %li.item.template %em http:// = text_field_tag 'label', t('formtastic.hints.site.domain_name'), :class => 'string label void' diff --git a/config/initializers/custom_fields.rb b/config/initializers/custom_fields.rb index 041439b1..5060ce00 100644 --- a/config/initializers/custom_fields.rb +++ b/config/initializers/custom_fields.rb @@ -50,7 +50,10 @@ module Mongoid #:nodoc: if self.custom_fields?(object, association_name) # puts "custom fields = #{object.asset_fields.inspect}" # puts "((((((((" - object.send(self.custom_fields_association_name(association_name)).each do |field| + + # puts " custom fields = #{self.custom_fields_association_name(association_name).inspect} / #{object.send(self.custom_fields_association_name(association_name)).inspect}" + + [*object.send(self.custom_fields_association_name(association_name))].each do |field| # puts "field = #{field.inspect}" # self.class.send(:set_field, field.name, { :type => field.field_type }) field.apply(self, association_name) diff --git a/config/locales/admin_ui_en.yml b/config/locales/admin_ui_en.yml index 45d0591d..eb6cb669 100644 --- a/config/locales/admin_ui_en.yml +++ b/config/locales/admin_ui_en.yml @@ -32,6 +32,11 @@ en: create: Create update: Update + custom_fields: + kind: + string: Simple Input + text: Text + sessions: new: title: Login @@ -149,6 +154,7 @@ en: file: File preview: Preview options: Advanced options + custom_fields: Custom fields labels: theme_asset: new: diff --git a/doc/TODO b/doc/TODO index 969225ff..37b74cc4 100644 --- a/doc/TODO +++ b/doc/TODO @@ -57,6 +57,9 @@ x domain scoping when authenticating - extract a plugin from custom fields - field position - nested attributes + - keep tracks of all custom fields (adding / editing assets) + - custom fields -> metadata keys + - duplicate fields BACKLOG: - liquid rendering engine diff --git a/public/javascripts/admin/custom_fields.js b/public/javascripts/admin/custom_fields.js new file mode 100644 index 00000000..3d7507bb --- /dev/null +++ b/public/javascripts/admin/custom_fields.js @@ -0,0 +1,113 @@ +$(document).ready(function() { + + $('fieldset.fields').parents('form').submit(function() { + $('fieldset.fields li.template input, fieldset.fields li.template select').attr('disabled', 'disabled'); + }); + + var defaultValue = $('fieldset.fields li.template input[type=text]').val(); + var selectOnChange = function(select) { + select.hide(); + select.prev() + .show() + .html(select[0].options[select[0].options.selectedIndex].text); + } + + var refreshPosition = function() { + jQuery.each($('fieldset.fields li.added input.position'), function(index) { + $(this).val(index); + }); + } + + /* __ fields ___ */ + $('fieldset.fields li.added select').each(function() { + var select = $(this) + .hover(function() { + clearTimeout(select.attr('timer')); + }, function() { + select.attr('timer', setTimeout(function() { + select.hide(); + select.prev().show(); + }, 1000)); + }) + .change(function() { selectOnChange(select); }); + + select.prev().click(function() { + $(this).hide(); + select.show(); + }); + }); + + $('fieldset.fields li.template input[type=text]').focus(function() { + if ($(this).hasClass('void') && $(this).parents('li').hasClass('template')) + $(this).val('').removeClass('void'); + }); + + $('fieldset.fields li.template button').click(function() { + var lastRow = $(this).parents('li.template'); + var newRow = lastRow.clone(true).removeClass('template').addClass('added new').insertBefore(lastRow); + + var dateFragment = '[' + new Date().getTime() + ']'; + newRow.find('input, select').each(function(index) { + $(this).attr('name', $(this).attr('name').replace('[-1]', dateFragment)); + }); + + // should copy the value of the select box + var input = newRow.find('input.label'); + if (lastRow.find('input.label').val() == '') input.val("undefined"); + + var select = newRow.find('select') + .val(lastRow.find('select').val()) + .change(function() { selectOnChange(select); }) + .hover(function() { + clearTimeout(select.attr('timer')); + }, function() { + select.attr('timer', setTimeout(function() { + select.hide(); + select.prev().show(); + }, 1000)); + }); + select.prev() + .html(select[0].options[select[0].options.selectedIndex].text) + .click(function() { + $(this).hide(); + select.show(); + }); + + // then reset the form + lastRow.find('input').val(defaultValue).addClass('void'); + lastRow.find('select').val('input'); + + // warn the sortable widget about the new row + $("fieldset.fields ol").sortable('refresh'); + + refreshPosition(); + }); + + $('fieldset.fields li a.remove').click(function(e) { + if (confirm($(this).attr('data-confirm'))) { + var parent = $(this).parents('li'); + + if (parent.hasClass('new')) + parent.remove(); + else { + var field = parent.find('input.position') + field.attr('name', field.attr('name').replace('[position]', '[_destroy]')); + + parent.hide().removeClass('added') + } + + refreshPosition(); + } + + e.preventDefault(); + e.stopPropagation(); + }); + + // sortable list + $("fieldset.fields ol").sortable({ + handle: 'span.handle', + items: 'li:not(.template)', + axis: 'y', + update: refreshPosition + }); +}); \ No newline at end of file diff --git a/public/javascripts/admin/site.js b/public/javascripts/admin/site.js index 42b2831f..1c5f5df3 100644 --- a/public/javascripts/admin/site.js +++ b/public/javascripts/admin/site.js @@ -1,20 +1,20 @@ $(document).ready(function() { - var defaultValue = $('fieldset.editable-list li.new input[type=text]').val(); + var defaultValue = $('fieldset.editable-list li.template input[type=text]').val(); /* __ fields ___ */ - $('fieldset.editable-list li.new input[type=text]').focus(function() { - if ($(this).hasClass('void') && $(this).parents('li').hasClass('new')) + $('fieldset.editable-list li.template input[type=text]').focus(function() { + if ($(this).hasClass('void') && $(this).parents('li').hasClass('template')) $(this).val('').removeClass('void'); }); - $('fieldset.editable-list li.new button').click(function() { - var lastRow = $(this).parents('li.new'); + $('fieldset.editable-list li.template button').click(function() { + var lastRow = $(this).parents('li.template'); var currentValue = lastRow.find('input.label').val(); if (currentValue == defaultValue || currentValue == '') return; - var newRow = lastRow.clone(true).removeClass('new').addClass('added').insertBefore(lastRow); + var newRow = lastRow.clone(true).removeClass('template').addClass('added').insertBefore(lastRow); // should copy the value of the select box var input = newRow.find('input.label') diff --git a/public/stylesheets/admin/formtastic_changes.css b/public/stylesheets/admin/formtastic_changes.css index 001993a4..021a6c28 100644 --- a/public/stylesheets/admin/formtastic_changes.css +++ b/public/stylesheets/admin/formtastic_changes.css @@ -302,13 +302,13 @@ form.formtastic fieldset.editable-list ol li.added .inline-errors { font-size: 0.8em; } -form.formtastic fieldset.editable-list ol li.new { +form.formtastic fieldset.editable-list ol li.template { height: 42px; background-image: url(/images/admin/form/big_item.png); padding-top: 10px; } -form.formtastic fieldset.editable-list ol li.new input { +form.formtastic fieldset.editable-list ol li.template input { display: inline; margin-left: 10px; padding: 4px; @@ -321,28 +321,28 @@ form.formtastic fieldset.editable-list ol li.new input { top: 1px; } -form.formtastic fieldset.editable-list ol li.new select { +form.formtastic fieldset.editable-list ol li.template select { display: inline; } -form.formtastic fieldset.editable-list ol li.new span.handle { +form.formtastic fieldset.editable-list ol li.template span.handle { display: none; } -form.formtastic fieldset.editable-list ol li.new span.actions { +form.formtastic fieldset.editable-list ol li.template span.actions { width: auto; top: 10px; } -form.formtastic fieldset.editable-list ol li.new span.actions a.remove { +form.formtastic fieldset.editable-list ol li.template span.actions a.remove { display: none; } -form.formtastic fieldset.editable-list ol li.new span.actions button { +form.formtastic fieldset.editable-list ol li.template span.actions button { display: inline; } -form.formtastic fieldset.editable-list ol li.new span.actions button span { +form.formtastic fieldset.editable-list ol li.template span.actions button span { font-size: 0.8em; } diff --git a/spec/models/asset_collections_spec.rb b/spec/models/asset_collections_spec.rb index f66a2938..39564a27 100644 --- a/spec/models/asset_collections_spec.rb +++ b/spec/models/asset_collections_spec.rb @@ -12,84 +12,138 @@ describe AssetCollection do @collection = Factory.build(:asset_collection, :site => nil) @collection.asset_fields.build :label => 'My Description', :_alias => 'description', :kind => 'Text' @collection.asset_fields.build :label => 'Active', :kind => 'Boolean' + puts "first field index = #{@collection.asset_fields.first._index}" end - context 'define core attributes' do - - it 'should have an unique name' do - @collection.asset_fields.first._name.should == "custom_field_1" - @collection.asset_fields.last._name.should == "custom_field_2" - end - - it 'should have an unique alias' do - @collection.asset_fields.first._alias.should == "description" - @collection.asset_fields.last._alias.should == "active" - end - - end + # context 'define core attributes' do + # + # it 'should have an unique name' do + # @collection.asset_fields.first._name.should == "custom_field_1" + # @collection.asset_fields.last._name.should == "custom_field_2" + # end + # + # it 'should have an unique alias' do + # @collection.asset_fields.first._alias.should == "description" + # @collection.asset_fields.last._alias.should == "active" + # end + # + # end + # + # context 'build and save' do + # + # it 'should build asset' do + # asset = @collection.assets.build + # lambda { + # asset.description + # asset.active + # }.should_not raise_error + # end + # + # it 'should assign values to newly built asset' do + # asset = build_asset(@collection) + # asset.description.should == 'Lorem ipsum' + # asset.active.should == true + # end + # + # it 'should save asset' do + # asset = build_asset(@collection) + # asset.save and @collection.reload + # asset = @collection.assets.first + # asset.description.should == 'Lorem ipsum' + # asset.active.should == true + # end + # + # it 'should not modify assets from another collection' do + # asset = build_asset(@collection) + # asset.save and @collection.reload + # new_collection = AssetCollection.new + # lambda { new_collection.assets.build.description }.should raise_error + # end + # + # end + # + # context 'modifying fields' do + # + # before(:each) do + # @asset = build_asset(@collection).save + # end + # + # it 'should add new field' do + # @collection.asset_fields.build :label => 'Active at', :name => 'active_at', :kind => 'Date' + # @collection.upsert(false) + # @collection.reload + # asset = @collection.assets.first + # lambda { asset.active_at }.should_not raise_error + # end + # + # it 'should remove field' do + # @collection.asset_fields.clear + # @collection.upsert(false) + # @collection.reload + # asset = @collection.assets.first + # lambda { asset.active }.should raise_error + # end + # + # it 'should rename field label' do + # @collection.asset_fields.first.label = 'Simple description' + # @collection.asset_fields.first._alias = nil + # @collection.upsert(false) + # @collection.reload + # asset = @collection.assets.first + # asset.simple_description.should == 'Lorem ipsum' + # end + # + # end - context 'build and save' do - - it 'should build asset' do - asset = @collection.assets.build - lambda { - asset.description - asset.active - }.should_not raise_error - end - - it 'should assign values to newly built asset' do - asset = build_asset(@collection) - asset.description.should == 'Lorem ipsum' - asset.active.should == true - end - - it 'should save asset' do - asset = build_asset(@collection) - asset.save and @collection.reload - asset = @collection.assets.first - asset.description.should == 'Lorem ipsum' - asset.active.should == true - end - - it 'should not modify assets from another collection' do - asset = build_asset(@collection) - asset.save and @collection.reload - new_collection = AssetCollection.new - lambda { new_collection.assets.build.description }.should raise_error - end - - end - - context 'modifying fields' do + context 'managing from hash' do before(:each) do - @asset = build_asset(@collection).save + # @collection.asset_fields.clear + # @collection.stubs(:validate).returns(true) + # @collection.stubs(:valid?).returns(true) + @collection.site = Factory(:site) end - it 'should add new field' do - @collection.asset_fields.build :label => 'Active at', :name => 'active_at', :kind => 'Date' - @collection.upsert(false) - @collection.reload - asset = @collection.assets.first - lambda { asset.active_at }.should_not raise_error - end + # it 'should add new field' do + # @collection.asset_fields.clear + # @collection.asset_fields_attributes = { 'NEW_RECORD' => { 'label' => 'Tagline', 'kind' => 'String' } } + # @collection.asset_fields.first.label.should == 'Tagline' + # end + # + # it 'should add new field' do + # @collection.asset_fields.build :label => 'Title' + # @collection.asset_fields_attributes = { '0' => { 'label' => 'A title', 'kind' => 'String' }, '-1' => { 'label' => 'Tagline', 'kind' => 'String' } } + # @collection.asset_fields.size.should == 2 + # @collection.asset_fields.first.label.should == 'A title' + # @collection.asset_fields.last.label.should == 'Tagline' + # end + # + # it 'should rename field' - it 'should remove field' do - @collection.asset_fields.clear - @collection.upsert(false) - @collection.reload - asset = @collection.assets.first - lambda { asset.active }.should raise_error - end - - it 'should rename field label' do - @collection.asset_fields.first.label = 'Simple description' - @collection.asset_fields.first._alias = nil - @collection.upsert(false) - @collection.reload - asset = @collection.assets.first - asset.simple_description.should == 'Lorem ipsum' + it 'should remove field' do + @collection.asset_fields.build :label => 'Title', :kind => 'String' + # puts @collection.asset_fields.collect { |d| d._index }.inspect + @collection.save + @collection = AssetCollection.first + foo = @collection.asset_fields + @collection.asset_fields.size.should == 3 + @collection.update_attributes(:asset_fields_attributes => { + # @collection.asset_fields_attributes = { + '0' => { 'label' => 'My Description', 'kind' => 'Text', '_destroy' => "1", "id" => foo[0].id }, + '1' => { 'label' => 'Active', 'kind' => 'Boolean', '_destroy' => "0", "id" => foo[1].id }, + '2' => { 'label' => 'My Title !', 'kind' => 'String', "id" => foo[2].id } + # } + }) + + puts @collection.raw_attributes.inspect + + # @collection.save + @collection = AssetCollection.first + # @collection.reload + puts "________________ #{@collection.asset_fields.class.inspect}" + puts @collection.asset_fields.inspect + @collection.asset_fields.size.should == 1 + @collection.asset_fields.first.label.should == 'My Title !' end end